diff options
235 files changed, 30815 insertions, 2561 deletions
diff --git a/Android.mk b/Android.mk index 21bd76b76a62..2539c3dfbda6 100644 --- a/Android.mk +++ b/Android.mk @@ -346,6 +346,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/appwidget/IAppWidgetHost.aidl \ core/java/com/android/internal/backup/IBackupTransport.aidl \ core/java/com/android/internal/backup/IObbBackupService.aidl \ + core/java/com/android/internal/font/IFontManager.aidl \ core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl \ core/java/com/android/internal/policy/IKeyguardDrawnCallback.aidl \ core/java/com/android/internal/policy/IKeyguardDismissCallback.aidl \ diff --git a/api/current.txt b/api/current.txt index 637cdac5b4fb..27b3bf368508 100644 --- a/api/current.txt +++ b/api/current.txt @@ -203,6 +203,8 @@ package android { public static final class R.attr { ctor public R.attr(); + field public static final int __removed0 = 16844097; // 0x1010541 + field public static final int __removed1 = 16844099; // 0x1010543 field public static final int absListViewStyle = 16842858; // 0x101006a field public static final int accessibilityEventTypes = 16843648; // 0x1010380 field public static final int accessibilityFeedbackType = 16843650; // 0x1010382 @@ -758,7 +760,6 @@ package android { field public static final int keyboardLayout = 16843691; // 0x10103ab field public static final int keyboardMode = 16843341; // 0x101024d field public static final int keyboardNavigationCluster = 16844096; // 0x1010540 - field public static final int keyboardNavigationSection = 16844097; // 0x1010541 field public static final int keycode = 16842949; // 0x10100c5 field public static final int killAfterRestore = 16843420; // 0x101029c field public static final int label = 16842753; // 0x1010001 @@ -908,7 +909,6 @@ package android { field public static final int nextFocusLeft = 16842977; // 0x10100e1 field public static final int nextFocusRight = 16842978; // 0x10100e2 field public static final int nextFocusUp = 16842979; // 0x10100e3 - field public static final int nextSectionForward = 16844099; // 0x1010543 field public static final int noHistory = 16843309; // 0x101022d field public static final int normalScreens = 16843397; // 0x1010285 field public static final int notificationTimeout = 16843651; // 0x1010383 @@ -8476,6 +8476,7 @@ package android.content { field public static final java.lang.String DOWNLOAD_SERVICE = "download"; field public static final java.lang.String DROPBOX_SERVICE = "dropbox"; field public static final java.lang.String FINGERPRINT_SERVICE = "fingerprint"; + field public static final java.lang.String FONT_SERVICE = "font"; field public static final java.lang.String HARDWARE_PROPERTIES_SERVICE = "hardware_properties"; field public static final java.lang.String INPUT_METHOD_SERVICE = "input_method"; field public static final java.lang.String INPUT_SERVICE = "input"; @@ -14114,6 +14115,37 @@ package android.hardware { method public float getZ(); } + public final class HardwareBuffer implements android.os.Parcelable { + method public static android.hardware.HardwareBuffer create(int, int, int, int, long); + method public int describeContents(); + method public void destroy(); + method public int getFormat(); + method public int getHeight(); + method public int getLayers(); + method public long getUsage(); + method public int getWidth(); + method public boolean isDestroyed(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR; + field public static final int RGBA_8888 = 1; // 0x1 + field public static final int RGBA_FP16 = 5; // 0x5 + field public static final int RGBX_8888 = 2; // 0x2 + field public static final int RGB_565 = 4; // 0x4 + field public static final int RGB_888 = 3; // 0x3 + field public static final long USAGE0_CPU_READ = 2L; // 0x2L + field public static final long USAGE0_CPU_READ_OFTEN = 6L; // 0x6L + field public static final long USAGE0_CPU_WRITE = 32L; // 0x20L + field public static final long USAGE0_CPU_WRITE_OFTEN = 96L; // 0x60L + field public static final long USAGE0_GPU_COLOR_OUTPUT = 2048L; // 0x800L + field public static final long USAGE0_GPU_CUBEMAP = 8192L; // 0x2000L + field public static final long USAGE0_GPU_DATA_BUFFER = 16384L; // 0x4000L + field public static final long USAGE0_GPU_SAMPLED_IMAGE = 1024L; // 0x400L + field public static final long USAGE0_GPU_STORAGE_IMAGE = 3072L; // 0xc00L + field public static final long USAGE0_PROTECTED_CONTENT = 262144L; // 0x40000L + field public static final long USAGE0_SENSOR_DIRECT_DATA = 536870912L; // 0x20000000L + field public static final long USAGE0_VIDEO_ENCODE = 2097152L; // 0x200000L + } + public final class Sensor { method public int getFifoMaxEventCount(); method public int getFifoReservedEventCount(); @@ -37695,7 +37727,7 @@ package android.telecom { field public static final java.lang.String ACTION_CHANGE_PHONE_ACCOUNTS = "android.telecom.action.CHANGE_PHONE_ACCOUNTS"; field public static final java.lang.String ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONFIGURE_PHONE_ACCOUNT"; field public static final java.lang.String ACTION_DEFAULT_DIALER_CHANGED = "android.telecom.action.DEFAULT_DIALER_CHANGED"; - field public static final java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; + field public static final deprecated java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; field public static final java.lang.String ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS = "android.telecom.action.SHOW_CALL_ACCESSIBILITY_SETTINGS"; field public static final java.lang.String ACTION_SHOW_CALL_SETTINGS = "android.telecom.action.SHOW_CALL_SETTINGS"; field public static final java.lang.String ACTION_SHOW_MISSED_CALLS_NOTIFICATION = "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"; @@ -37798,7 +37830,8 @@ package android.telephony { field public static final java.lang.String KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL = "carrier_volte_provisioning_required_bool"; field public static final java.lang.String KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL = "carrier_volte_tty_supported_bool"; field public static final java.lang.String KEY_CARRIER_VT_AVAILABLE_BOOL = "carrier_vt_available_bool"; - field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final deprecated java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY = "carrier_vvm_package_name_string_array"; field public static final java.lang.String KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL = "carrier_wfc_ims_available_bool"; field public static final java.lang.String KEY_CARRIER_WFC_SUPPORTS_WIFI_ONLY_BOOL = "carrier_wfc_supports_wifi_only_bool"; field public static final java.lang.String KEY_CDMA_3WAYCALL_FLASH_DELAY_INT = "cdma_3waycall_flash_delay_int"; @@ -37890,9 +37923,13 @@ package android.telephony { field public static final java.lang.String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool"; field public static final java.lang.String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int"; field public static final java.lang.String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool"; + field public static final java.lang.String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string"; field public static final java.lang.String KEY_VVM_DESTINATION_NUMBER_STRING = "vvm_destination_number_string"; + field public static final java.lang.String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY = "vvm_disabled_capabilities_string_array"; + field public static final java.lang.String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool"; field public static final java.lang.String KEY_VVM_PORT_NUMBER_INT = "vvm_port_number_int"; field public static final java.lang.String KEY_VVM_PREFETCH_BOOL = "vvm_prefetch_bool"; + field public static final java.lang.String KEY_VVM_SSL_ENABLED_BOOL = "vvm_ssl_enabled_bool"; field public static final java.lang.String KEY_VVM_TYPE_STRING = "vvm_type_string"; field public static final java.lang.String KEY_WORLD_PHONE_BOOL = "world_phone_bool"; } @@ -39454,6 +39491,65 @@ package android.text { method public android.text.Editable newEditable(java.lang.CharSequence); } + public final class FontConfig implements android.os.Parcelable { + ctor public FontConfig(); + ctor public FontConfig(android.text.FontConfig); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Alias> getAliases(); + method public java.util.List<android.text.FontConfig.Family> getFamilies(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig> CREATOR; + } + + public static final class FontConfig.Alias implements android.os.Parcelable { + ctor public FontConfig.Alias(java.lang.String, java.lang.String, int); + method public int describeContents(); + method public java.lang.String getName(); + method public java.lang.String getToName(); + method public int getWeight(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Alias> CREATOR; + } + + public static final class FontConfig.Axis implements android.os.Parcelable { + ctor public FontConfig.Axis(int, float); + method public int describeContents(); + method public float getStyleValue(); + method public int getTag(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Axis> CREATOR; + } + + public static final class FontConfig.Family implements android.os.Parcelable { + ctor public FontConfig.Family(java.lang.String, java.util.List<android.text.FontConfig.Font>, java.lang.String, java.lang.String); + ctor public FontConfig.Family(android.text.FontConfig.Family); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Font> getFonts(); + method public java.lang.String getLanguage(); + method public java.lang.String getName(); + method public java.lang.String getVariant(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Family> CREATOR; + } + + public static final class FontConfig.Font implements android.os.Parcelable { + ctor public FontConfig.Font(java.lang.String, int, java.util.List<android.text.FontConfig.Axis>, int, boolean); + ctor public FontConfig.Font(android.text.FontConfig.Font); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Axis> getAxes(); + method public android.os.ParcelFileDescriptor getFd(); + method public java.lang.String getFontName(); + method public int getTtcIndex(); + method public int getWeight(); + method public boolean isItalic(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Font> CREATOR; + } + + public final class FontManager { + method public android.text.FontConfig getSystemFonts(); + } + public abstract interface GetChars implements java.lang.CharSequence { method public abstract void getChars(int, int, char[], int); } @@ -42119,7 +42215,9 @@ package android.view { method public android.view.Display.Mode[] getSupportedModes(); method public deprecated float[] getSupportedRefreshRates(); method public deprecated int getWidth(); + method public boolean isHdr(); method public boolean isValid(); + method public boolean isWideColorGamut(); field public static final int DEFAULT_DISPLAY = 0; // 0x0 field public static final int FLAG_PRESENTATION = 8; // 0x8 field public static final int FLAG_PRIVATE = 4; // 0x4 @@ -42188,7 +42286,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationGroup(int, android.view.View, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -43486,7 +43584,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList<android.view.View>); method public void addFocusables(java.util.ArrayList<android.view.View>, int); method public void addFocusables(java.util.ArrayList<android.view.View>, int, int); - method public void addKeyboardNavigationGroups(int, java.util.Collection<android.view.View>, int); + method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList<android.view.View>); @@ -43650,7 +43748,6 @@ package android.view { method public int getNextFocusLeftId(); method public int getNextFocusRightId(); method public int getNextFocusUpId(); - method public int getNextSectionForwardId(); method public android.view.View.OnFocusChangeListener getOnFocusChangeListener(); method public android.view.ViewOutlineProvider getOutlineProvider(); method public int getOverScrollMode(); @@ -43756,7 +43853,6 @@ package android.view { method public boolean isInLayout(); method public boolean isInTouchMode(); method public final boolean isKeyboardNavigationCluster(); - method public final boolean isKeyboardNavigationSection(); method public boolean isLaidOut(); method public boolean isLayoutDirectionResolved(); method public boolean isLayoutRequested(); @@ -43779,7 +43875,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -43929,7 +44025,6 @@ package android.view { method public void setImportantForAccessibility(int); method public void setKeepScreenOn(boolean); method public void setKeyboardNavigationCluster(boolean); - method public void setKeyboardNavigationSection(boolean); method public void setLabelFor(int); method public void setLayerPaint(android.graphics.Paint); method public void setLayerType(int, android.graphics.Paint); @@ -43947,7 +44042,6 @@ package android.view { method public void setNextFocusLeftId(int); method public void setNextFocusRightId(int); method public void setNextFocusUpId(int); - method public void setNextSectionForwardId(int); method public void setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener); method public void setOnClickListener(android.view.View.OnClickListener); method public void setOnContextClickListener(android.view.View.OnContextClickListener); @@ -44074,8 +44168,6 @@ package android.view { field public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 1; // 0x1 field public static final int INVISIBLE = 4; // 0x4 field public static final int KEEP_SCREEN_ON = 67108864; // 0x4000000 - field public static final int KEYBOARD_NAVIGATION_GROUP_CLUSTER = 1; // 0x1 - field public static final int KEYBOARD_NAVIGATION_GROUP_SECTION = 2; // 0x2 field public static final int LAYER_TYPE_HARDWARE = 2; // 0x2 field public static final int LAYER_TYPE_NONE = 0; // 0x0 field public static final int LAYER_TYPE_SOFTWARE = 1; // 0x1 @@ -44594,7 +44686,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); diff --git a/api/system-current.txt b/api/system-current.txt index bafc3657cf8f..dc043d988589 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -312,6 +312,8 @@ package android { public static final class R.attr { ctor public R.attr(); + field public static final int __removed0 = 16844097; // 0x1010541 + field public static final int __removed1 = 16844099; // 0x1010543 field public static final int absListViewStyle = 16842858; // 0x101006a field public static final int accessibilityEventTypes = 16843648; // 0x1010380 field public static final int accessibilityFeedbackType = 16843650; // 0x1010382 @@ -867,7 +869,6 @@ package android { field public static final int keyboardLayout = 16843691; // 0x10103ab field public static final int keyboardMode = 16843341; // 0x101024d field public static final int keyboardNavigationCluster = 16844096; // 0x1010540 - field public static final int keyboardNavigationSection = 16844097; // 0x1010541 field public static final int keycode = 16842949; // 0x10100c5 field public static final int killAfterRestore = 16843420; // 0x101029c field public static final int label = 16842753; // 0x1010001 @@ -1017,7 +1018,6 @@ package android { field public static final int nextFocusLeft = 16842977; // 0x10100e1 field public static final int nextFocusRight = 16842978; // 0x10100e2 field public static final int nextFocusUp = 16842979; // 0x10100e3 - field public static final int nextSectionForward = 16844099; // 0x1010543 field public static final int noHistory = 16843309; // 0x101022d field public static final int normalScreens = 16843397; // 0x1010285 field public static final int notificationTimeout = 16843651; // 0x1010383 @@ -5504,9 +5504,11 @@ package android.app { ctor public Notification.TvExtender(); ctor public Notification.TvExtender(android.app.Notification); method public android.app.Notification.Builder extend(android.app.Notification.Builder); + method public java.lang.String getChannel(); method public android.app.PendingIntent getContentIntent(); method public android.app.PendingIntent getDeleteIntent(); method public boolean isAvailableOnTv(); + method public android.app.Notification.TvExtender setChannel(java.lang.String); method public android.app.Notification.TvExtender setContentIntent(android.app.PendingIntent); method public android.app.Notification.TvExtender setDeleteIntent(android.app.PendingIntent); } @@ -8852,6 +8854,7 @@ package android.content { field public static final java.lang.String DOWNLOAD_SERVICE = "download"; field public static final java.lang.String DROPBOX_SERVICE = "dropbox"; field public static final java.lang.String FINGERPRINT_SERVICE = "fingerprint"; + field public static final java.lang.String FONT_SERVICE = "font"; field public static final java.lang.String HARDWARE_PROPERTIES_SERVICE = "hardware_properties"; field public static final java.lang.String HDMI_CONTROL_SERVICE = "hdmi_control"; field public static final java.lang.String INPUT_METHOD_SERVICE = "input_method"; @@ -10240,6 +10243,15 @@ package android.content.pm { field public java.lang.String targetPackage; } + public final class IntentFilterVerificationInfo implements android.os.Parcelable { + method public int describeContents(); + method public java.util.Set<java.lang.String> getDomains(); + method public java.lang.String getPackageName(); + method public int getStatus(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.content.pm.IntentFilterVerificationInfo> CREATOR; + } + public class LabeledIntent extends android.content.Intent { ctor public LabeledIntent(android.content.Intent, java.lang.String, int, int); ctor public LabeledIntent(android.content.Intent, java.lang.String, java.lang.CharSequence, int); @@ -10505,6 +10517,7 @@ package android.content.pm { method public abstract android.content.pm.ActivityInfo getActivityInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; method public abstract android.graphics.drawable.Drawable getActivityLogo(android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException; method public abstract android.graphics.drawable.Drawable getActivityLogo(android.content.Intent) throws android.content.pm.PackageManager.NameNotFoundException; + method public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(java.lang.String); method public abstract java.util.List<android.content.pm.PermissionGroupInfo> getAllPermissionGroups(int); method public abstract android.graphics.drawable.Drawable getApplicationBanner(android.content.pm.ApplicationInfo); method public abstract android.graphics.drawable.Drawable getApplicationBanner(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; @@ -10517,12 +10530,15 @@ package android.content.pm { method public abstract android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public abstract int getComponentEnabledSetting(android.content.ComponentName); method public abstract android.graphics.drawable.Drawable getDefaultActivityIcon(); + method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int); method public abstract android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo); method public abstract java.util.List<android.content.pm.ApplicationInfo> getInstalledApplications(int); method public abstract java.util.List<android.content.pm.PackageInfo> getInstalledPackages(int); method public abstract java.util.List<android.content.pm.PackageInfo> getInstalledPackagesAsUser(int, int); method public abstract java.lang.String getInstallerPackageName(java.lang.String); method public abstract android.content.pm.InstrumentationInfo getInstrumentationInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; + method public abstract java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(java.lang.String); + method public abstract int getIntentVerificationStatusAsUser(java.lang.String, int); method public abstract android.content.Intent getLaunchIntentForPackage(java.lang.String); method public abstract android.content.Intent getLeanbackLaunchIntentForPackage(java.lang.String); method public abstract java.lang.String getNameForUid(int); @@ -10578,7 +10594,9 @@ package android.content.pm { method public abstract void setApplicationCategoryHint(java.lang.String, int); method public abstract void setApplicationEnabledSetting(java.lang.String, int, int); method public abstract void setComponentEnabledSetting(android.content.ComponentName, int, int); + method public abstract boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); method public abstract void setInstallerPackageName(java.lang.String, java.lang.String); + method public abstract boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); method public abstract void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle); method public abstract void verifyIntentFilter(int, int, java.util.List<java.lang.String>); method public abstract void verifyPendingInstall(int, int); @@ -10739,6 +10757,11 @@ package android.content.pm { field public static final int INSTALL_REASON_POLICY = 1; // 0x1 field public static final int INSTALL_REASON_UNKNOWN = 0; // 0x0 field public static final int INSTALL_SUCCEEDED = 1; // 0x1 + field public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS = 2; // 0x2 + field public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK = 4; // 0x4 + field public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK = 1; // 0x1 + field public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER = 3; // 0x3 + field public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED = 0; // 0x0 field public static final int INTENT_FILTER_VERIFICATION_FAILURE = -1; // 0xffffffff field public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1 field public static final int MASK_PERMISSION_FLAGS = 255; // 0xff @@ -14637,6 +14660,37 @@ package android.hardware { method public float getZ(); } + public final class HardwareBuffer implements android.os.Parcelable { + method public static android.hardware.HardwareBuffer create(int, int, int, int, long); + method public int describeContents(); + method public void destroy(); + method public int getFormat(); + method public int getHeight(); + method public int getLayers(); + method public long getUsage(); + method public int getWidth(); + method public boolean isDestroyed(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR; + field public static final int RGBA_8888 = 1; // 0x1 + field public static final int RGBA_FP16 = 5; // 0x5 + field public static final int RGBX_8888 = 2; // 0x2 + field public static final int RGB_565 = 4; // 0x4 + field public static final int RGB_888 = 3; // 0x3 + field public static final long USAGE0_CPU_READ = 2L; // 0x2L + field public static final long USAGE0_CPU_READ_OFTEN = 6L; // 0x6L + field public static final long USAGE0_CPU_WRITE = 32L; // 0x20L + field public static final long USAGE0_CPU_WRITE_OFTEN = 96L; // 0x60L + field public static final long USAGE0_GPU_COLOR_OUTPUT = 2048L; // 0x800L + field public static final long USAGE0_GPU_CUBEMAP = 8192L; // 0x2000L + field public static final long USAGE0_GPU_DATA_BUFFER = 16384L; // 0x4000L + field public static final long USAGE0_GPU_SAMPLED_IMAGE = 1024L; // 0x400L + field public static final long USAGE0_GPU_STORAGE_IMAGE = 3072L; // 0xc00L + field public static final long USAGE0_PROTECTED_CONTENT = 262144L; // 0x40000L + field public static final long USAGE0_SENSOR_DIRECT_DATA = 536870912L; // 0x20000000L + field public static final long USAGE0_VIDEO_ENCODE = 2097152L; // 0x200000L + } + public final class Sensor { method public int getFifoMaxEventCount(); method public int getFifoReservedEventCount(); @@ -40878,7 +40932,7 @@ package android.telecom { field public static final java.lang.String ACTION_CHANGE_PHONE_ACCOUNTS = "android.telecom.action.CHANGE_PHONE_ACCOUNTS"; field public static final java.lang.String ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONFIGURE_PHONE_ACCOUNT"; field public static final java.lang.String ACTION_DEFAULT_DIALER_CHANGED = "android.telecom.action.DEFAULT_DIALER_CHANGED"; - field public static final java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; + field public static final deprecated java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; field public static final java.lang.String ACTION_PHONE_ACCOUNT_REGISTERED = "android.telecom.action.PHONE_ACCOUNT_REGISTERED"; field public static final java.lang.String ACTION_PHONE_ACCOUNT_UNREGISTERED = "android.telecom.action.PHONE_ACCOUNT_UNREGISTERED"; field public static final java.lang.String ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS = "android.telecom.action.SHOW_CALL_ACCESSIBILITY_SETTINGS"; @@ -40988,7 +41042,8 @@ package android.telephony { field public static final java.lang.String KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL = "carrier_volte_provisioning_required_bool"; field public static final java.lang.String KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL = "carrier_volte_tty_supported_bool"; field public static final java.lang.String KEY_CARRIER_VT_AVAILABLE_BOOL = "carrier_vt_available_bool"; - field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final deprecated java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY = "carrier_vvm_package_name_string_array"; field public static final java.lang.String KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL = "carrier_wfc_ims_available_bool"; field public static final java.lang.String KEY_CARRIER_WFC_SUPPORTS_WIFI_ONLY_BOOL = "carrier_wfc_supports_wifi_only_bool"; field public static final java.lang.String KEY_CDMA_3WAYCALL_FLASH_DELAY_INT = "cdma_3waycall_flash_delay_int"; @@ -41080,9 +41135,13 @@ package android.telephony { field public static final java.lang.String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool"; field public static final java.lang.String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int"; field public static final java.lang.String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool"; + field public static final java.lang.String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string"; field public static final java.lang.String KEY_VVM_DESTINATION_NUMBER_STRING = "vvm_destination_number_string"; + field public static final java.lang.String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY = "vvm_disabled_capabilities_string_array"; + field public static final java.lang.String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool"; field public static final java.lang.String KEY_VVM_PORT_NUMBER_INT = "vvm_port_number_int"; field public static final java.lang.String KEY_VVM_PREFETCH_BOOL = "vvm_prefetch_bool"; + field public static final java.lang.String KEY_VVM_SSL_ENABLED_BOOL = "vvm_ssl_enabled_bool"; field public static final java.lang.String KEY_VVM_TYPE_STRING = "vvm_type_string"; field public static final java.lang.String KEY_WORLD_PHONE_BOOL = "world_phone_bool"; } @@ -42467,12 +42526,15 @@ package android.test.mock { method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public int getComponentEnabledSetting(android.content.ComponentName); method public android.graphics.drawable.Drawable getDefaultActivityIcon(); + method public java.lang.String getDefaultBrowserPackageNameAsUser(int); method public android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo); method public java.util.List<android.content.pm.ApplicationInfo> getInstalledApplications(int); method public java.util.List<android.content.pm.PackageInfo> getInstalledPackages(int); method public java.util.List<android.content.pm.PackageInfo> getInstalledPackagesAsUser(int, int); method public java.lang.String getInstallerPackageName(java.lang.String); method public android.content.pm.InstrumentationInfo getInstrumentationInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; + method public java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(java.lang.String); + method public int getIntentVerificationStatusAsUser(java.lang.String, int); method public android.content.Intent getLaunchIntentForPackage(java.lang.String); method public android.content.Intent getLeanbackLaunchIntentForPackage(java.lang.String); method public java.lang.String getNameForUid(int); @@ -42526,7 +42588,9 @@ package android.test.mock { method public void setApplicationCategoryHint(java.lang.String, int); method public void setApplicationEnabledSetting(java.lang.String, int, int); method public void setComponentEnabledSetting(android.content.ComponentName, int, int); + method public boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); method public void setInstallerPackageName(java.lang.String, java.lang.String); + method public boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); method public void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle); method public void verifyIntentFilter(int, int, java.util.List<java.lang.String>); method public void verifyPendingInstall(int, int); @@ -42730,6 +42794,65 @@ package android.text { method public android.text.Editable newEditable(java.lang.CharSequence); } + public final class FontConfig implements android.os.Parcelable { + ctor public FontConfig(); + ctor public FontConfig(android.text.FontConfig); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Alias> getAliases(); + method public java.util.List<android.text.FontConfig.Family> getFamilies(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig> CREATOR; + } + + public static final class FontConfig.Alias implements android.os.Parcelable { + ctor public FontConfig.Alias(java.lang.String, java.lang.String, int); + method public int describeContents(); + method public java.lang.String getName(); + method public java.lang.String getToName(); + method public int getWeight(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Alias> CREATOR; + } + + public static final class FontConfig.Axis implements android.os.Parcelable { + ctor public FontConfig.Axis(int, float); + method public int describeContents(); + method public float getStyleValue(); + method public int getTag(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Axis> CREATOR; + } + + public static final class FontConfig.Family implements android.os.Parcelable { + ctor public FontConfig.Family(java.lang.String, java.util.List<android.text.FontConfig.Font>, java.lang.String, java.lang.String); + ctor public FontConfig.Family(android.text.FontConfig.Family); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Font> getFonts(); + method public java.lang.String getLanguage(); + method public java.lang.String getName(); + method public java.lang.String getVariant(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Family> CREATOR; + } + + public static final class FontConfig.Font implements android.os.Parcelable { + ctor public FontConfig.Font(java.lang.String, int, java.util.List<android.text.FontConfig.Axis>, int, boolean); + ctor public FontConfig.Font(android.text.FontConfig.Font); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Axis> getAxes(); + method public android.os.ParcelFileDescriptor getFd(); + method public java.lang.String getFontName(); + method public int getTtcIndex(); + method public int getWeight(); + method public boolean isItalic(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Font> CREATOR; + } + + public final class FontManager { + method public android.text.FontConfig getSystemFonts(); + } + public abstract interface GetChars implements java.lang.CharSequence { method public abstract void getChars(int, int, char[], int); } @@ -44695,6 +44818,7 @@ package android.util { method public static int getTagCode(java.lang.String); method public static java.lang.String getTagName(int); method public static void readEvents(int[], java.util.Collection<android.util.EventLog.Event>) throws java.io.IOException; + method public static void readEventsOnWrapping(int[], long, java.util.Collection<android.util.EventLog.Event>) throws java.io.IOException; method public static int writeEvent(int, int); method public static int writeEvent(int, long); method public static int writeEvent(int, float); @@ -45395,7 +45519,9 @@ package android.view { method public android.view.Display.Mode[] getSupportedModes(); method public deprecated float[] getSupportedRefreshRates(); method public deprecated int getWidth(); + method public boolean isHdr(); method public boolean isValid(); + method public boolean isWideColorGamut(); field public static final int DEFAULT_DISPLAY = 0; // 0x0 field public static final int FLAG_PRESENTATION = 8; // 0x8 field public static final int FLAG_PRIVATE = 4; // 0x4 @@ -45464,7 +45590,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationGroup(int, android.view.View, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -46762,7 +46888,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList<android.view.View>); method public void addFocusables(java.util.ArrayList<android.view.View>, int); method public void addFocusables(java.util.ArrayList<android.view.View>, int, int); - method public void addKeyboardNavigationGroups(int, java.util.Collection<android.view.View>, int); + method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList<android.view.View>); @@ -46926,7 +47052,6 @@ package android.view { method public int getNextFocusLeftId(); method public int getNextFocusRightId(); method public int getNextFocusUpId(); - method public int getNextSectionForwardId(); method public android.view.View.OnFocusChangeListener getOnFocusChangeListener(); method public android.view.ViewOutlineProvider getOutlineProvider(); method public int getOverScrollMode(); @@ -47032,7 +47157,6 @@ package android.view { method public boolean isInLayout(); method public boolean isInTouchMode(); method public final boolean isKeyboardNavigationCluster(); - method public final boolean isKeyboardNavigationSection(); method public boolean isLaidOut(); method public boolean isLayoutDirectionResolved(); method public boolean isLayoutRequested(); @@ -47055,7 +47179,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -47205,7 +47329,6 @@ package android.view { method public void setImportantForAccessibility(int); method public void setKeepScreenOn(boolean); method public void setKeyboardNavigationCluster(boolean); - method public void setKeyboardNavigationSection(boolean); method public void setLabelFor(int); method public void setLayerPaint(android.graphics.Paint); method public void setLayerType(int, android.graphics.Paint); @@ -47223,7 +47346,6 @@ package android.view { method public void setNextFocusLeftId(int); method public void setNextFocusRightId(int); method public void setNextFocusUpId(int); - method public void setNextSectionForwardId(int); method public void setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener); method public void setOnClickListener(android.view.View.OnClickListener); method public void setOnContextClickListener(android.view.View.OnContextClickListener); @@ -47350,8 +47472,6 @@ package android.view { field public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 1; // 0x1 field public static final int INVISIBLE = 4; // 0x4 field public static final int KEEP_SCREEN_ON = 67108864; // 0x4000000 - field public static final int KEYBOARD_NAVIGATION_GROUP_CLUSTER = 1; // 0x1 - field public static final int KEYBOARD_NAVIGATION_GROUP_SECTION = 2; // 0x2 field public static final int LAYER_TYPE_HARDWARE = 2; // 0x2 field public static final int LAYER_TYPE_NONE = 0; // 0x0 field public static final int LAYER_TYPE_SOFTWARE = 1; // 0x1 @@ -47870,7 +47990,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); @@ -50597,6 +50717,7 @@ package android.webkit { method public java.lang.String getErrorString(android.content.Context, int); method public int getPackageId(android.content.res.Resources, java.lang.String); method public void invokeDrawGlFunctor(android.view.View, long, boolean); + method public boolean isMultiProcessEnabled(); method public boolean isTraceTagEnabled(); method public void setOnTraceEnabledChangeListener(android.webkit.WebViewDelegate.OnTraceEnabledChangeListener); } diff --git a/api/test-current.txt b/api/test-current.txt index c4081f0d4b4d..b6b7140c50b3 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -203,6 +203,8 @@ package android { public static final class R.attr { ctor public R.attr(); + field public static final int __removed0 = 16844097; // 0x1010541 + field public static final int __removed1 = 16844099; // 0x1010543 field public static final int absListViewStyle = 16842858; // 0x101006a field public static final int accessibilityEventTypes = 16843648; // 0x1010380 field public static final int accessibilityFeedbackType = 16843650; // 0x1010382 @@ -758,7 +760,6 @@ package android { field public static final int keyboardLayout = 16843691; // 0x10103ab field public static final int keyboardMode = 16843341; // 0x101024d field public static final int keyboardNavigationCluster = 16844096; // 0x1010540 - field public static final int keyboardNavigationSection = 16844097; // 0x1010541 field public static final int keycode = 16842949; // 0x10100c5 field public static final int killAfterRestore = 16843420; // 0x101029c field public static final int label = 16842753; // 0x1010001 @@ -908,7 +909,6 @@ package android { field public static final int nextFocusLeft = 16842977; // 0x10100e1 field public static final int nextFocusRight = 16842978; // 0x10100e2 field public static final int nextFocusUp = 16842979; // 0x10100e3 - field public static final int nextSectionForward = 16844099; // 0x1010543 field public static final int noHistory = 16843309; // 0x101022d field public static final int normalScreens = 16843397; // 0x1010285 field public static final int notificationTimeout = 16843651; // 0x1010383 @@ -8500,6 +8500,7 @@ package android.content { field public static final java.lang.String DOWNLOAD_SERVICE = "download"; field public static final java.lang.String DROPBOX_SERVICE = "dropbox"; field public static final java.lang.String FINGERPRINT_SERVICE = "fingerprint"; + field public static final java.lang.String FONT_SERVICE = "font"; field public static final java.lang.String HARDWARE_PROPERTIES_SERVICE = "hardware_properties"; field public static final java.lang.String INPUT_METHOD_SERVICE = "input_method"; field public static final java.lang.String INPUT_SERVICE = "input"; @@ -14146,6 +14147,37 @@ package android.hardware { method public float getZ(); } + public final class HardwareBuffer implements android.os.Parcelable { + method public static android.hardware.HardwareBuffer create(int, int, int, int, long); + method public int describeContents(); + method public void destroy(); + method public int getFormat(); + method public int getHeight(); + method public int getLayers(); + method public long getUsage(); + method public int getWidth(); + method public boolean isDestroyed(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.hardware.HardwareBuffer> CREATOR; + field public static final int RGBA_8888 = 1; // 0x1 + field public static final int RGBA_FP16 = 5; // 0x5 + field public static final int RGBX_8888 = 2; // 0x2 + field public static final int RGB_565 = 4; // 0x4 + field public static final int RGB_888 = 3; // 0x3 + field public static final long USAGE0_CPU_READ = 2L; // 0x2L + field public static final long USAGE0_CPU_READ_OFTEN = 6L; // 0x6L + field public static final long USAGE0_CPU_WRITE = 32L; // 0x20L + field public static final long USAGE0_CPU_WRITE_OFTEN = 96L; // 0x60L + field public static final long USAGE0_GPU_COLOR_OUTPUT = 2048L; // 0x800L + field public static final long USAGE0_GPU_CUBEMAP = 8192L; // 0x2000L + field public static final long USAGE0_GPU_DATA_BUFFER = 16384L; // 0x4000L + field public static final long USAGE0_GPU_SAMPLED_IMAGE = 1024L; // 0x400L + field public static final long USAGE0_GPU_STORAGE_IMAGE = 3072L; // 0xc00L + field public static final long USAGE0_PROTECTED_CONTENT = 262144L; // 0x40000L + field public static final long USAGE0_SENSOR_DIRECT_DATA = 536870912L; // 0x20000000L + field public static final long USAGE0_VIDEO_ENCODE = 2097152L; // 0x200000L + } + public final class Sensor { method public int getFifoMaxEventCount(); method public int getFifoReservedEventCount(); @@ -37816,7 +37848,7 @@ package android.telecom { field public static final java.lang.String ACTION_CHANGE_PHONE_ACCOUNTS = "android.telecom.action.CHANGE_PHONE_ACCOUNTS"; field public static final java.lang.String ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONFIGURE_PHONE_ACCOUNT"; field public static final java.lang.String ACTION_DEFAULT_DIALER_CHANGED = "android.telecom.action.DEFAULT_DIALER_CHANGED"; - field public static final java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; + field public static final deprecated java.lang.String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; field public static final java.lang.String ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS = "android.telecom.action.SHOW_CALL_ACCESSIBILITY_SETTINGS"; field public static final java.lang.String ACTION_SHOW_CALL_SETTINGS = "android.telecom.action.SHOW_CALL_SETTINGS"; field public static final java.lang.String ACTION_SHOW_MISSED_CALLS_NOTIFICATION = "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"; @@ -37919,7 +37951,8 @@ package android.telephony { field public static final java.lang.String KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL = "carrier_volte_provisioning_required_bool"; field public static final java.lang.String KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL = "carrier_volte_tty_supported_bool"; field public static final java.lang.String KEY_CARRIER_VT_AVAILABLE_BOOL = "carrier_vt_available_bool"; - field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final deprecated java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; + field public static final java.lang.String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY = "carrier_vvm_package_name_string_array"; field public static final java.lang.String KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL = "carrier_wfc_ims_available_bool"; field public static final java.lang.String KEY_CARRIER_WFC_SUPPORTS_WIFI_ONLY_BOOL = "carrier_wfc_supports_wifi_only_bool"; field public static final java.lang.String KEY_CDMA_3WAYCALL_FLASH_DELAY_INT = "cdma_3waycall_flash_delay_int"; @@ -38011,9 +38044,13 @@ package android.telephony { field public static final java.lang.String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool"; field public static final java.lang.String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int"; field public static final java.lang.String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool"; + field public static final java.lang.String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string"; field public static final java.lang.String KEY_VVM_DESTINATION_NUMBER_STRING = "vvm_destination_number_string"; + field public static final java.lang.String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY = "vvm_disabled_capabilities_string_array"; + field public static final java.lang.String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool"; field public static final java.lang.String KEY_VVM_PORT_NUMBER_INT = "vvm_port_number_int"; field public static final java.lang.String KEY_VVM_PREFETCH_BOOL = "vvm_prefetch_bool"; + field public static final java.lang.String KEY_VVM_SSL_ENABLED_BOOL = "vvm_ssl_enabled_bool"; field public static final java.lang.String KEY_VVM_TYPE_STRING = "vvm_type_string"; field public static final java.lang.String KEY_WORLD_PHONE_BOOL = "world_phone_bool"; } @@ -39578,6 +39615,65 @@ package android.text { method public android.text.Editable newEditable(java.lang.CharSequence); } + public final class FontConfig implements android.os.Parcelable { + ctor public FontConfig(); + ctor public FontConfig(android.text.FontConfig); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Alias> getAliases(); + method public java.util.List<android.text.FontConfig.Family> getFamilies(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig> CREATOR; + } + + public static final class FontConfig.Alias implements android.os.Parcelable { + ctor public FontConfig.Alias(java.lang.String, java.lang.String, int); + method public int describeContents(); + method public java.lang.String getName(); + method public java.lang.String getToName(); + method public int getWeight(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Alias> CREATOR; + } + + public static final class FontConfig.Axis implements android.os.Parcelable { + ctor public FontConfig.Axis(int, float); + method public int describeContents(); + method public float getStyleValue(); + method public int getTag(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Axis> CREATOR; + } + + public static final class FontConfig.Family implements android.os.Parcelable { + ctor public FontConfig.Family(java.lang.String, java.util.List<android.text.FontConfig.Font>, java.lang.String, java.lang.String); + ctor public FontConfig.Family(android.text.FontConfig.Family); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Font> getFonts(); + method public java.lang.String getLanguage(); + method public java.lang.String getName(); + method public java.lang.String getVariant(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Family> CREATOR; + } + + public static final class FontConfig.Font implements android.os.Parcelable { + ctor public FontConfig.Font(java.lang.String, int, java.util.List<android.text.FontConfig.Axis>, int, boolean); + ctor public FontConfig.Font(android.text.FontConfig.Font); + method public int describeContents(); + method public java.util.List<android.text.FontConfig.Axis> getAxes(); + method public android.os.ParcelFileDescriptor getFd(); + method public java.lang.String getFontName(); + method public int getTtcIndex(); + method public int getWeight(); + method public boolean isItalic(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.text.FontConfig.Font> CREATOR; + } + + public final class FontManager { + method public android.text.FontConfig getSystemFonts(); + } + public abstract interface GetChars implements java.lang.CharSequence { method public abstract void getChars(int, int, char[], int); } @@ -42408,7 +42504,9 @@ package android.view { method public android.view.Display.Mode[] getSupportedModes(); method public deprecated float[] getSupportedRefreshRates(); method public deprecated int getWidth(); + method public boolean isHdr(); method public boolean isValid(); + method public boolean isWideColorGamut(); field public static final int DEFAULT_DISPLAY = 0; // 0x0 field public static final int FLAG_PRESENTATION = 8; // 0x8 field public static final int FLAG_PRIVATE = 4; // 0x4 @@ -42477,7 +42575,7 @@ package android.view { method public android.view.View findNearestTouchable(android.view.ViewGroup, int, int, int, int[]); method public final android.view.View findNextFocus(android.view.ViewGroup, android.view.View, int); method public android.view.View findNextFocusFromRect(android.view.ViewGroup, android.graphics.Rect, int); - method public android.view.View findNextKeyboardNavigationGroup(int, android.view.View, android.view.View, int); + method public android.view.View findNextKeyboardNavigationCluster(android.view.View, android.view.View, int); method public static android.view.FocusFinder getInstance(); } @@ -43777,7 +43875,7 @@ package android.view { method public void addChildrenForAccessibility(java.util.ArrayList<android.view.View>); method public void addFocusables(java.util.ArrayList<android.view.View>, int); method public void addFocusables(java.util.ArrayList<android.view.View>, int, int); - method public void addKeyboardNavigationGroups(int, java.util.Collection<android.view.View>, int); + method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener); method public void addTouchables(java.util.ArrayList<android.view.View>); @@ -43941,7 +44039,6 @@ package android.view { method public int getNextFocusLeftId(); method public int getNextFocusRightId(); method public int getNextFocusUpId(); - method public int getNextSectionForwardId(); method public android.view.View.OnFocusChangeListener getOnFocusChangeListener(); method public android.view.ViewOutlineProvider getOutlineProvider(); method public int getOverScrollMode(); @@ -44048,7 +44145,6 @@ package android.view { method public boolean isInLayout(); method public boolean isInTouchMode(); method public final boolean isKeyboardNavigationCluster(); - method public final boolean isKeyboardNavigationSection(); method public boolean isLaidOut(); method public boolean isLayoutDirectionResolved(); method public boolean isLayoutRequested(); @@ -44071,7 +44167,7 @@ package android.view { method public boolean isVerticalFadingEdgeEnabled(); method public boolean isVerticalScrollBarEnabled(); method public void jumpDrawablesToCurrentState(); - method public android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public void layout(int, int, int, int); method public final void measure(int, int); method protected static int[] mergeDrawableStates(int[], int[]); @@ -44221,7 +44317,6 @@ package android.view { method public void setImportantForAccessibility(int); method public void setKeepScreenOn(boolean); method public void setKeyboardNavigationCluster(boolean); - method public void setKeyboardNavigationSection(boolean); method public void setLabelFor(int); method public void setLayerPaint(android.graphics.Paint); method public void setLayerType(int, android.graphics.Paint); @@ -44239,7 +44334,6 @@ package android.view { method public void setNextFocusLeftId(int); method public void setNextFocusRightId(int); method public void setNextFocusUpId(int); - method public void setNextSectionForwardId(int); method public void setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener); method public void setOnClickListener(android.view.View.OnClickListener); method public void setOnContextClickListener(android.view.View.OnContextClickListener); @@ -44366,8 +44460,6 @@ package android.view { field public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 1; // 0x1 field public static final int INVISIBLE = 4; // 0x4 field public static final int KEEP_SCREEN_ON = 67108864; // 0x4000000 - field public static final int KEYBOARD_NAVIGATION_GROUP_CLUSTER = 1; // 0x1 - field public static final int KEYBOARD_NAVIGATION_GROUP_SECTION = 2; // 0x2 field public static final int LAYER_TYPE_HARDWARE = 2; // 0x2 field public static final int LAYER_TYPE_NONE = 0; // 0x0 field public static final int LAYER_TYPE_SOFTWARE = 1; // 0x1 @@ -44890,7 +44982,7 @@ package android.view { method public abstract boolean isLayoutRequested(); method public abstract boolean isTextAlignmentResolved(); method public abstract boolean isTextDirectionResolved(); - method public abstract android.view.View keyboardNavigationGroupSearch(int, android.view.View, int); + method public abstract android.view.View keyboardNavigationClusterSearch(android.view.View, int); method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int); method public abstract boolean onNestedFling(android.view.View, float, float, boolean); method public abstract boolean onNestedPreFling(android.view.View, float, float); diff --git a/compiled-classes-phone b/compiled-classes-phone index ebc54f2128e9..ed0a4a6a33cc 100644 --- a/compiled-classes-phone +++ b/compiled-classes-phone @@ -1195,13 +1195,13 @@ android.graphics.DashPathEffect android.graphics.DiscretePathEffect android.graphics.DrawFilter android.graphics.EmbossMaskFilter +android.graphics.FontConfig +android.graphics.FontConfig$Alias +android.graphics.FontConfig$Axis +android.graphics.FontConfig$Family +android.graphics.FontConfig$Font android.graphics.FontFamily android.graphics.FontListParser -android.graphics.FontListParser$Alias -android.graphics.FontListParser$Axis -android.graphics.FontListParser$Config -android.graphics.FontListParser$Family -android.graphics.FontListParser$Font android.graphics.ImageFormat android.graphics.Insets android.graphics.Interpolator diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 1d4b038f2d82..c1a888d28428 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -520,17 +520,17 @@ public class ActivityManager { /** @hide Flag for registerUidObserver: report uid has become active. */ public static final int UID_OBSERVER_ACTIVE = 1<<3; - /** @hide Mode for {@link IActivityManager#getAppStartMode}: normal free-to-run operation. */ + /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: normal free-to-run operation. */ public static final int APP_START_MODE_NORMAL = 0; - /** @hide Mode for {@link IActivityManager#getAppStartMode}: delay running until later. */ + /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: delay running until later. */ public static final int APP_START_MODE_DELAYED = 1; - /** @hide Mode for {@link IActivityManager#getAppStartMode}: delay running until later, with + /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: delay running until later, with * rigid errors (throwing exception). */ public static final int APP_START_MODE_DELAYED_RIGID = 2; - /** @hide Mode for {@link IActivityManager#getAppStartMode}: disable/cancel pending + /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: disable/cancel pending * launches; this is the mode for ephemeral apps. */ public static final int APP_START_MODE_DISABLED = 3; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 1658e8261a02..d37888d76802 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1341,8 +1341,8 @@ class ContextImpl extends Context { } try { final Intent intent = ActivityManager.getService().registerReceiver( - mMainThread.getApplicationThread(), mBasePackageName, - rd, filter, broadcastPermission, userId); + mMainThread.getApplicationThread(), mBasePackageName, rd, filter, + broadcastPermission, userId); if (intent != null) { intent.setExtrasClassLoader(getClassLoader()); intent.prepareToEnterProcess(); @@ -1591,12 +1591,15 @@ class ContextImpl extends Context { } final IActivityManager am = ActivityManager.getService(); - if (am == null && UserHandle.getAppId(Binder.getCallingUid()) == Process.SYSTEM_UID) { + if (am == null) { // Well this is super awkward; we somehow don't have an active - // ActivityManager instance. If this is the system UID, then we - // totally have whatever permission this is. - Slog.w(TAG, "Missing ActivityManager; assuming system UID holds " + permission); - return PackageManager.PERMISSION_GRANTED; + // ActivityManager instance. If we're testing a root or system + // UID, then they totally have whatever permission this is. + final int appId = UserHandle.getAppId(uid); + if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) { + Slog.w(TAG, "Missing ActivityManager; assuming " + uid + " holds " + permission); + return PackageManager.PERMISSION_GRANTED; + } } try { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 135c2a4eef91..5a4879367a97 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -264,7 +264,7 @@ interface IActivityManager { boolean isImmersive(in IBinder token); void setImmersive(in IBinder token, boolean immersive); boolean isTopActivityImmersive(); - void crashApplication(int uid, int initialPid, in String packageName, in String message); + void crashApplication(int uid, int initialPid, in String packageName, int userId, in String message); String getProviderMimeType(in Uri uri, int userId); IBinder newUriPermissionOwner(in String name); void grantUriPermissionFromOwner(in IBinder owner, int fromUid, in String targetPkg, @@ -475,7 +475,7 @@ interface IActivityManager { void suppressResizeConfigChanges(boolean suppress); void moveTasksToFullscreenStack(int fromStackId, boolean onTop); boolean moveTopActivityToPinnedStack(int stackId, in Rect bounds); - int getAppStartMode(int uid, in String packageName); + boolean isAppStartModeDisabled(int uid, in String packageName); boolean unlockUser(int userid, in byte[] token, in byte[] secret, in IProgressListener listener); boolean isInMultiWindowMode(in IBinder token); diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 036b47cafe64..6793c90f9be1 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -322,7 +322,7 @@ public class KeyguardManager { * password. */ public boolean isDeviceLocked() { - return isDeviceLocked(UserHandle.getCallingUserId()); + return isDeviceLocked(UserHandle.myUserId()); } /** @@ -347,7 +347,7 @@ public class KeyguardManager { * @return true if a PIN, pattern or password was set. */ public boolean isDeviceSecure() { - return isDeviceSecure(UserHandle.getCallingUserId()); + return isDeviceSecure(UserHandle.myUserId()); } /** diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 5b74e23da7c9..601dfceb3a4a 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -7088,11 +7088,13 @@ public class Notification implements Parcelable private static final String EXTRA_FLAGS = "flags"; private static final String EXTRA_CONTENT_INTENT = "content_intent"; private static final String EXTRA_DELETE_INTENT = "delete_intent"; + private static final String EXTRA_CHANNEL_ID = "channel_id"; // Flags bitwise-ored to mFlags private static final int FLAG_AVAILABLE_ON_TV = 0x1; private int mFlags; + private String mChannelId; private PendingIntent mContentIntent; private PendingIntent mDeleteIntent; @@ -7113,6 +7115,7 @@ public class Notification implements Parcelable null : notif.extras.getBundle(EXTRA_TV_EXTENDER); if (bundle != null) { mFlags = bundle.getInt(EXTRA_FLAGS); + mChannelId = bundle.getString(EXTRA_CHANNEL_ID); mContentIntent = bundle.getParcelable(EXTRA_CONTENT_INTENT); mDeleteIntent = bundle.getParcelable(EXTRA_DELETE_INTENT); } @@ -7128,6 +7131,7 @@ public class Notification implements Parcelable Bundle bundle = new Bundle(); bundle.putInt(EXTRA_FLAGS, mFlags); + bundle.putString(EXTRA_CHANNEL_ID, mChannelId); if (mContentIntent != null) { bundle.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent); } @@ -7149,6 +7153,23 @@ public class Notification implements Parcelable } /** + * Specifies the channel the notification should be delivered on when shown on TV. + * It can be different from the channel that the notification is delivered to when + * posting on a non-TV device. + */ + public TvExtender setChannel(String channelId) { + mChannelId = channelId; + return this; + } + + /** + * Returns the id of the channel this notification posts to on TV. + */ + public String getChannel() { + return mChannelId; + } + + /** * Supplies a {@link PendingIntent} to be sent when the notification is selected on TV. * If provided, it is used instead of the content intent specified * at the level of Notification. diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9387019bc651..a37f22b888b0 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -118,6 +118,7 @@ import android.telecom.TelecomManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.text.FontManager; import android.text.TextClassificationManager; import android.util.Log; import android.view.ContextThemeWrapper; @@ -133,6 +134,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.font.IFontManager; import com.android.internal.os.IDropBoxManagerService; import com.android.internal.policy.PhoneLayoutInflater; @@ -793,6 +795,15 @@ final class SystemServiceRegistry { public IncidentManager createService(ContextImpl ctx) throws ServiceNotFoundException { return new IncidentManager(ctx); }}); + + registerService(Context.FONT_SERVICE, FontManager.class, + new CachedServiceFetcher<FontManager>() { + @Override + public FontManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow(Context.FONT_SERVICE); + return new FontManager(IFontManager.Stub.asInterface(b)); + }}); } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 596a9fd0610a..f41d7f2f168d 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3744,6 +3744,11 @@ public abstract class Context { public static final String DEVICE_IDENTIFIERS_SERVICE = "device_identifiers"; /** + * Service that provides System font data. + */ + public static final String FONT_SERVICE = "font"; + + /** * Service to report a system health "incident" * @hide */ diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 8cc9a3aecb70..c5500949b25f 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -4842,6 +4842,10 @@ public class Intent implements Parcelable, Cloneable { * or not running) apps, regardless of whether that would be done by default. By * default they will only receive broadcasts if the broadcast has specified an * explicit component or package name. + * + * NOTE: dumpstate uses this flag numerically, so when its value is changed + * the broadcast code there must also be changed to match. + * * @hide */ public static final int FLAG_RECEIVER_INCLUDE_BACKGROUND = 0x01000000; diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java index f12abf36c172..068973b84d38 100644 --- a/core/java/android/content/pm/IntentFilterVerificationInfo.java +++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java @@ -22,6 +22,7 @@ import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATIO import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -36,6 +37,7 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.ArrayList; +import java.util.Set; /** * The {@link com.android.server.pm.PackageManagerService} maintains some @@ -43,6 +45,7 @@ import java.util.ArrayList; * * @hide */ +@SystemApi public final class IntentFilterVerificationInfo implements Parcelable { private static final String TAG = IntentFilterVerificationInfo.class.getName(); @@ -55,22 +58,26 @@ public final class IntentFilterVerificationInfo implements Parcelable { private String mPackageName; private int mMainStatus; + /** @hide */ public IntentFilterVerificationInfo() { mPackageName = null; mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; } + /** @hide */ public IntentFilterVerificationInfo(String packageName, ArraySet<String> domains) { mPackageName = packageName; mDomains = domains; mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; } + /** @hide */ public IntentFilterVerificationInfo(XmlPullParser parser) throws IOException, XmlPullParserException { readFromXml(parser); } + /** @hide */ public IntentFilterVerificationInfo(Parcel source) { readFromParcel(source); } @@ -83,6 +90,7 @@ public final class IntentFilterVerificationInfo implements Parcelable { return mMainStatus; } + /** @hide */ public void setStatus(int s) { if (s >= INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED && s <= INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) { @@ -92,14 +100,16 @@ public final class IntentFilterVerificationInfo implements Parcelable { } } - public ArraySet<String> getDomains() { + public Set<String> getDomains() { return mDomains; } + /** @hide */ public void setDomains(ArraySet<String> list) { mDomains = list; } + /** @hide */ public String getDomainsString() { StringBuilder sb = new StringBuilder(); for (String str : mDomains) { @@ -135,6 +145,7 @@ public final class IntentFilterVerificationInfo implements Parcelable { } } + /** @hide */ public void readFromXml(XmlPullParser parser) throws XmlPullParserException, IOException { mPackageName = getStringFromXml(parser, ATTR_PACKAGE_NAME, null); @@ -170,6 +181,7 @@ public final class IntentFilterVerificationInfo implements Parcelable { } } + /** @hide */ public void writeToXml(XmlSerializer serializer) throws IOException { serializer.attribute(null, ATTR_PACKAGE_NAME, mPackageName); serializer.attribute(null, ATTR_STATUS, String.valueOf(mMainStatus)); @@ -180,10 +192,12 @@ public final class IntentFilterVerificationInfo implements Parcelable { } } + /** @hide */ public String getStatusString() { return getStatusStringFromValue(((long)mMainStatus) << 32); } + /** @hide */ public static String getStatusStringFromValue(long val) { StringBuilder sb = new StringBuilder(); switch ((int)(val >> 32)) { diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 7bdc56db5300..98edbf817519 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1444,6 +1444,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED = 0; /** @@ -1454,6 +1455,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK = 1; /** @@ -1465,6 +1467,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS = 2; /** @@ -1476,6 +1479,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER = 3; /** @@ -1489,6 +1493,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK = 4; /** @@ -5070,6 +5075,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public abstract int getIntentVerificationStatusAsUser(String packageName, @UserIdInt int userId); /** @@ -5092,6 +5098,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public abstract boolean updateIntentVerificationStatusAsUser(String packageName, int status, @UserIdInt int userId); @@ -5107,6 +5114,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public abstract List<IntentFilterVerificationInfo> getIntentFilterVerifications( String packageName); @@ -5121,6 +5129,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public abstract List<IntentFilter> getAllIntentFilters(String packageName); /** @@ -5134,6 +5143,7 @@ public abstract class PackageManager { * @hide */ @TestApi + @SystemApi public abstract String getDefaultBrowserPackageNameAsUser(@UserIdInt int userId); /** @@ -5148,6 +5158,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public abstract boolean setDefaultBrowserPackageNameAsUser(String packageName, @UserIdInt int userId); diff --git a/core/java/android/hardware/HardwareBuffer.aidl b/core/java/android/hardware/HardwareBuffer.aidl new file mode 100644 index 000000000000..5bdd96677da5 --- /dev/null +++ b/core/java/android/hardware/HardwareBuffer.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +parcelable HardwareBuffer; diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java new file mode 100644 index 000000000000..fffb1d77d27c --- /dev/null +++ b/core/java/android/hardware/HardwareBuffer.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import dalvik.annotation.optimization.FastNative; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import libcore.util.NativeAllocationRegistry; + +/** + * HardwareBuffer wraps a native <code>AHardwareBuffer</code> object, which is a low-level object + * representing a memory buffer accessible by various hardware units. HardwareBuffer allows sharing + * buffers across different application processes. In particular, HardwareBuffers may be mappable + * to memory accessibly to various hardware systems, such as the GPU, a sensor or context hub, or + * other auxiliary processing units. + * + * For more information, see the NDK documentation for <code>AHardwareBuffer</code>. + */ +public final class HardwareBuffer implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RGBA_8888, RGBA_FP16, RGBX_8888, RGB_888, RGB_565}) + public @interface Format {}; + + /** Format: 8 bits each red, green, blue, alpha */ + public static final int RGBA_8888 = 1; + /** Format: 8 bits each red, green, blue, alpha, alpha is always 0xFF */ + public static final int RGBX_8888 = 2; + /** Format: 8 bits each red, green, blue, no alpha */ + public static final int RGB_888 = 3; + /** Format: 5 bits each red and blue, 6 bits green, no alpha */ + public static final int RGB_565 = 4; + /** Format: 16 bits each red, green, blue, alpha */ + public static final int RGBA_FP16 = 5; + + // Note: do not rename, this field is used by native code + private long mNativeObject; + + // Invoked on destruction + private Runnable mCleaner; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {USAGE0_CPU_READ, USAGE0_CPU_READ_OFTEN, USAGE0_CPU_WRITE, + USAGE0_CPU_WRITE_OFTEN, USAGE0_GPU_SAMPLED_IMAGE, USAGE0_GPU_COLOR_OUTPUT, + USAGE0_GPU_STORAGE_IMAGE, USAGE0_GPU_CUBEMAP, USAGE0_GPU_DATA_BUFFER, + USAGE0_PROTECTED_CONTENT, USAGE0_SENSOR_DIRECT_DATA, USAGE0_VIDEO_ENCODE}) + public @interface Usage0 {}; + + /** Usage0: the buffer will sometimes be read by the CPU */ + public static final long USAGE0_CPU_READ = (1 << 1); + /** Usage0: the buffer will often be read by the CPU*/ + public static final long USAGE0_CPU_READ_OFTEN = (1 << 2 | USAGE0_CPU_READ); + /** Usage0: the buffer will sometimes be written to by the CPU */ + public static final long USAGE0_CPU_WRITE = (1 << 5); + /** Usage0: the buffer will often be written to by the CPU */ + public static final long USAGE0_CPU_WRITE_OFTEN = (1 << 6 | USAGE0_CPU_WRITE); + /** Usage0: the buffer will be read from by the GPU */ + public static final long USAGE0_GPU_SAMPLED_IMAGE = (1 << 10); + /** Usage0: the buffer will be written to by the GPU */ + public static final long USAGE0_GPU_COLOR_OUTPUT = (1 << 11); + /** Usage0: the buffer will be read from and written to by the GPU */ + public static final long USAGE0_GPU_STORAGE_IMAGE = (USAGE0_GPU_SAMPLED_IMAGE | + USAGE0_GPU_COLOR_OUTPUT); + /** Usage0: the buffer will be used as a cubemap texture */ + public static final long USAGE0_GPU_CUBEMAP = (1 << 13); + /** Usage0: the buffer will be used as a shader storage or uniform buffer object*/ + public static final long USAGE0_GPU_DATA_BUFFER = (1 << 14); + /** Usage0: the buffer must not be used outside of a protected hardware path */ + public static final long USAGE0_PROTECTED_CONTENT = (1 << 18); + /** Usage0: the buffer will be used for sensor direct data */ + public static final long USAGE0_SENSOR_DIRECT_DATA = (1 << 29); + /** Usage0: the buffer will be read by a hardware video encoder */ + public static final long USAGE0_VIDEO_ENCODE = (1 << 21); + + // The approximate size of a native AHardwareBuffer object. + private static final long NATIVE_HARDWARE_BUFFER_SIZE = 232; + /** + * Creates a new <code>HardwareBuffer</code> instance. + * + * <p>Calling this method will throw an <code>IllegalStateException</code> if + * format is not a supported Format type.</p> + * + * @param width The width in pixels of the buffer + * @param height The height in pixels of the buffer + * @param format The format of each pixel, one of {@link #RGBA_8888}, {@link #RGBA_FP16}, + * {@link #RGBX_8888}, {@link #RGB_565}, {@link #RGB_888} + * @param layers The number of layers in the buffer + * @param usage Flags describing how the buffer will be used, one of + * {@link #USAGE0_CPU_READ}, {@link #USAGE0_CPU_READ_OFTEN}, {@link #USAGE0_CPU_WRITE}, + * {@link #USAGE0_CPU_WRITE_OFTEN}, {@link #USAGE0_GPU_SAMPLED_IMAGE}, + * {@link #USAGE0_GPU_COLOR_OUTPUT},{@link #USAGE0_GPU_STORAGE_IMAGE}, + * {@link #USAGE0_GPU_CUBEMAP}, {@link #USAGE0_GPU_DATA_BUFFER}, + * {@link #USAGE0_PROTECTED_CONTENT}, {@link #USAGE0_SENSOR_DIRECT_DATA}, + * {@link #USAGE0_VIDEO_ENCODE} + * + * @return A <code>HardwareBuffer</code> instance if successful, or throws an + * IllegalArgumentException if the dimensions passed are invalid (either zero, negative, or + * too large to allocate), if the format is not supported, if the requested number of layers + * is less than one or not supported, or if the passed usage flags are not a supported set. + */ + @NonNull + public static HardwareBuffer create(int width, int height, @Format int format, int layers, + @Usage0 long usage) { + if (!HardwareBuffer.isSupportedFormat(format)) { + throw new IllegalArgumentException("Invalid pixel format " + format); + } + if (width <= 0) { + throw new IllegalArgumentException("Invalid width " + width); + } + if (height <= 0) { + throw new IllegalArgumentException("Invalid height " + height); + } + if (layers <= 0) { + throw new IllegalArgumentException("Invalid layer count " + layers); + } + long nativeObject = nCreateHardwareBuffer(width, height, format, layers, usage); + if (nativeObject == 0) { + throw new IllegalArgumentException("Unable to create a HardwareBuffer, either the " + + "dimensions passed were too large, too many image layers were requested, " + + "or an invalid set of usage flags was passed"); + } + return new HardwareBuffer(nativeObject); + } + + /** + * Private use only. See {@link #create(int, int, int, int, int, long, long)}. May also be + * called from JNI using an already allocated native <code>HardwareBuffer</code>. + */ + private HardwareBuffer(long nativeObject) { + mNativeObject = nativeObject; + + long nativeSize = NATIVE_HARDWARE_BUFFER_SIZE; + NativeAllocationRegistry registry = new NativeAllocationRegistry( + HardwareBuffer.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); + mCleaner = registry.registerNativeAllocation(this, mNativeObject); + } + + /** + * Returns the width of this buffer in pixels. + */ + public int getWidth() { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and its width " + + "cannot be obtained."); + } + return nGetWidth(mNativeObject); + } + + /** + * Returns the height of this buffer in pixels. + */ + public int getHeight() { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and its height " + + "cannot be obtained."); + } + return nGetHeight(mNativeObject); + } + + /** + * Returns the format of this buffer, one of {@link #RGBA_8888}, {@link #RGBA_FP16}, + * {@link #RGBX_8888}, {@link #RGB_565}, or {@link #RGB_888}. + */ + public int getFormat() { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and its format " + + "cannot be obtained."); + } + return nGetFormat(mNativeObject); + } + + /** + * Returns the number of layers in this buffer. + */ + public int getLayers() { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and its layer " + + "count cannot be obtained."); + } + return nGetLayers(mNativeObject); + } + + /** + * Returns the usage flags of the usage hints set on this buffer. + */ + public long getUsage() { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and its usage " + + "cannot be obtained."); + } + return nGetUsage(mNativeObject); + } + + /** + * Destroys this buffer immediately. Calling this method frees up any + * underlying native resources. After calling this method, this buffer + * must not be used in any way. + * + * @see #isDestroyed() + */ + public void destroy() { + if (mNativeObject != 0) { + mNativeObject = 0; + mCleaner.run(); + mCleaner = null; + } + } + + /** + * Indicates whether this buffer has been destroyed. A destroyed buffer + * cannot be used in any way: the buffer cannot be written to a parcel, etc. + * + * @return True if this <code>HardwareBuffer</code> is in a destroyed state, + * false otherwise. + * + * @see #destroy() + */ + public boolean isDestroyed() { + return mNativeObject == 0; + } + + @Override + public int describeContents() { + return Parcelable.CONTENTS_FILE_DESCRIPTOR; + } + + /** + * Flatten this object in to a Parcel. + * + * <p>Calling this method will throw an <code>IllegalStateException</code> if + * {@link #destroy()} has been previously called.</p> + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mNativeObject == 0) { + throw new IllegalStateException("This HardwareBuffer has been destroyed and cannot be " + + "written to a parcel."); + } + nWriteHardwareBufferToParcel(mNativeObject, dest); + } + + public static final Parcelable.Creator<HardwareBuffer> CREATOR = + new Parcelable.Creator<HardwareBuffer>() { + public HardwareBuffer createFromParcel(Parcel in) { + long nativeObject = nReadHardwareBufferFromParcel(in); + if (nativeObject != 0) { + return new HardwareBuffer(nativeObject); + } + return null; + } + + public HardwareBuffer[] newArray(int size) { + return new HardwareBuffer[size]; + } + }; + + /** + * Validates whether a particular format is supported by HardwareBuffer. + * + * @param format The format to validate. + * + * @return True if <code>format</code> is a supported format. false otherwise. + * See {@link #create(int, int, int, int, int, long, long)}.a + */ + private static boolean isSupportedFormat(@Format int format) { + switch(format) { + case RGBA_8888: + case RGBA_FP16: + case RGBX_8888: + case RGB_565: + case RGB_888: + return true; + } + return false; + } + + private static native long nCreateHardwareBuffer(int width, int height, int format, int layers, + long usage); + private static native long nGetNativeFinalizer(); + private static native void nWriteHardwareBufferToParcel(long nativeObject, Parcel dest); + private static native long nReadHardwareBufferFromParcel(Parcel in); + @FastNative + private static native int nGetWidth(long nativeObject); + @FastNative + private static native int nGetHeight(long nativeObject); + @FastNative + private static native int nGetFormat(long nativeObject); + @FastNative + private static native int nGetLayers(long nativeObject); + @FastNative + private static native long nGetUsage(long nativeObject); +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5dcc96ac27d4..71b9482657b6 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7601,6 +7601,14 @@ public final class Settings { "hdmi_control_auto_device_off_enabled"; /** + * The interval in milliseconds at which location requests will be throttled when they are + * coming from the background. + * @hide + */ + public static final String LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS = + "location_background_throttle_interval_ms"; + + /** * Whether TV will switch to MHL port when a mobile device is plugged in. * (0 = false, 1 = true) * @hide diff --git a/core/java/android/text/FontConfig.aidl b/core/java/android/text/FontConfig.aidl new file mode 100644 index 000000000000..17a5ca2d7830 --- /dev/null +++ b/core/java/android/text/FontConfig.aidl @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +/** @hide */ +parcelable FontConfig; diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java new file mode 100644 index 000000000000..df694ff6af31 --- /dev/null +++ b/core/java/android/text/FontConfig.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Font configuration descriptions for System fonts. + */ +public final class FontConfig implements Parcelable { + private final List<Family> mFamilies = new ArrayList<>(); + private final List<Alias> mAliases = new ArrayList<>(); + + public FontConfig() { + } + + public FontConfig(FontConfig config) { + for (int i = 0; i < config.mFamilies.size(); i++) { + mFamilies.add(new Family(config.mFamilies.get(i))); + } + mAliases.addAll(config.mAliases); + } + + /** + * Returns the ordered list of families included in the system fonts. + */ + public List<Family> getFamilies() { + return mFamilies; + } + + /** + * Returns the list of aliases defined for the font families in the system fonts. + */ + public List<Alias> getAliases() { + return mAliases; + } + + /** + * @hide + */ + public FontConfig(Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeInt(mFamilies.size()); + for (int i = 0; i < mFamilies.size(); i++) { + mFamilies.get(i).writeToParcel(out, flag); + } + out.writeInt(mAliases.size()); + for (int i = 0; i < mAliases.size(); i++) { + mAliases.get(i).writeToParcel(out, flag); + } + } + + /** + * @hide + */ + public void readFromParcel(Parcel in) { + int size = in.readInt(); + for (int i = 0; i < size; i++) { + mFamilies.add(new Family(in)); + } + size = in.readInt(); + for (int i = 0; i < size; i++) { + mAliases.add(new Alias(in)); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<FontConfig> CREATOR = new Parcelable.Creator() { + public FontConfig createFromParcel(Parcel in) { + return new FontConfig(in); + } + public FontConfig[] newArray(int size) { + return new FontConfig[size]; + } + }; + + /** + * Class that holds information about a Font axis. + */ + public static final class Axis implements Parcelable { + private final int mTag; + private final float mStyleValue; + + public Axis(int tag, float styleValue) { + this.mTag = tag; + this.mStyleValue = styleValue; + } + + /** + * Returns the variable font axis tag associated to this axis. + */ + public int getTag() { + return mTag; + } + + /** + * Returns the style value associated to the given axis for this font. + */ + public float getStyleValue() { + return mStyleValue; + } + + /** + * @hide + */ + public Axis(Parcel in) { + mTag = in.readInt(); + mStyleValue = in.readFloat(); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeInt(mTag); + out.writeFloat(mStyleValue); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Axis> CREATOR = new Creator<Axis>() { + @Override + public Axis createFromParcel(Parcel in) { + return new Axis(in); + } + + @Override + public Axis[] newArray(int size) { + return new Axis[size]; + } + }; + } + + /** + * Class that holds information about a Font. + */ + public static final class Font implements Parcelable { + private final String mFontName; + private final int mTtcIndex; + private final List<Axis> mAxes; + private final int mWeight; + private final boolean mIsItalic; + private ParcelFileDescriptor mFd; + + public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) { + mFontName = fontName; + mTtcIndex = ttcIndex; + mAxes = axes; + mWeight = weight; + mIsItalic = isItalic; + mFd = null; + } + + public Font(Font origin) { + mFontName = origin.mFontName; + mTtcIndex = origin.mTtcIndex; + mAxes = new ArrayList<>(origin.mAxes); + mWeight = origin.mWeight; + mIsItalic = origin.mIsItalic; + if (origin.mFd != null) { + try { + mFd = origin.mFd.dup(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * Returns the name associated by the system to this font. + */ + public String getFontName() { + return mFontName; + } + + /** + * Returns the index to be used to access this font when accessing a TTC file. + */ + public int getTtcIndex() { + return mTtcIndex; + } + + /** + * Returns the list of axes associated to this font. + */ + public List<Axis> getAxes() { + return mAxes; + } + + /** + * Returns the weight value for this font. + */ + public int getWeight() { + return mWeight; + } + + /** + * Returns whether this font is italic. + */ + public boolean isItalic() { + return mIsItalic; + } + + /** + * Returns a file descriptor to access the specified font. This should be closed after use. + */ + public ParcelFileDescriptor getFd() { + return mFd; + } + + /** + * @hide + */ + public void setFd(ParcelFileDescriptor fd) { + mFd = fd; + } + + /** + * @hide + */ + public Font(Parcel in) { + mFontName = in.readString(); + mTtcIndex = in.readInt(); + final int numAxes = in.readInt(); + mAxes = new ArrayList<>(); + for (int i = 0; i < numAxes; i++) { + mAxes.add(new Axis(in)); + } + mWeight = in.readInt(); + mIsItalic = in.readInt() == 1; + if (in.readInt() == 1) { /* has FD */ + mFd = ParcelFileDescriptor.CREATOR.createFromParcel(in); + } else { + mFd = null; + } + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeString(mFontName); + out.writeInt(mTtcIndex); + out.writeInt(mAxes.size()); + for (int i = 0; i < mAxes.size(); i++) { + mAxes.get(i).writeToParcel(out, flag); + } + out.writeInt(mWeight); + out.writeInt(mIsItalic ? 1 : 0); + out.writeInt(mFd == null ? 0 : 1); + if (mFd != null) { + mFd.writeToParcel(out, flag); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Font> CREATOR = new Creator<Font>() { + @Override + public Font createFromParcel(Parcel in) { + return new Font(in); + } + + @Override + public Font[] newArray(int size) { + return new Font[size]; + } + }; + } + + /** + * Class that holds information about a Font alias. + */ + public static final class Alias implements Parcelable { + private final String mName; + private final String mToName; + private final int mWeight; + + public Alias(String name, String toName, int weight) { + this.mName = name; + this.mToName = toName; + this.mWeight = weight; + } + + /** + * Returns the new name for the alias. + */ + public String getName() { + return mName; + } + + /** + * Returns the existing name to which this alias points to. + */ + public String getToName() { + return mToName; + } + + /** + * Returns the weight associated with this alias. + */ + public int getWeight() { + return mWeight; + } + + /** + * @hide + */ + public Alias(Parcel in) { + mName = in.readString(); + mToName = in.readString(); + mWeight = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeString(mName); + out.writeString(mToName); + out.writeInt(mWeight); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Alias> CREATOR = new Creator<Alias>() { + @Override + public Alias createFromParcel(Parcel in) { + return new Alias(in); + } + + @Override + public Alias[] newArray(int size) { + return new Alias[size]; + } + }; + } + + /** + * Class that holds information about a Font family. + */ + public static final class Family implements Parcelable { + private final String mName; + private final List<Font> mFonts; + private final String mLanguage; + private final String mVariant; + + public Family(String name, List<Font> fonts, String language, String variant) { + this.mName = name; + this.mFonts = fonts; + this.mLanguage = language; + this.mVariant = variant; + } + + public Family(Family origin) { + this.mName = origin.mName; + this.mLanguage = origin.mLanguage; + this.mVariant = origin.mVariant; + this.mFonts = new ArrayList<>(); + for (int i = 0; i < origin.mFonts.size(); i++) { + mFonts.add(new Font(origin.mFonts.get(i))); + } + } + + /** + * Returns the name given by the system to this font family. + */ + public String getName() { + return mName; + } + + /** + * Returns the list of fonts included in this family. + */ + public List<Font> getFonts() { + return mFonts; + } + + /** + * Returns the language for this family. May be null. + */ + public String getLanguage() { + return mLanguage; + } + + /** + * Returns the font variant for this family, e.g. "elegant" or "compact". May be null. + */ + public String getVariant() { + return mVariant; + } + + /** + * @hide + */ + public Family(Parcel in) { + mName = in.readString(); + final int size = in.readInt(); + mFonts = new ArrayList<>(); + for (int i = 0; i < size; i++) { + mFonts.add(new Font(in)); + } + mLanguage = in.readString(); + mVariant = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeString(mName); + out.writeInt(mFonts.size()); + for (int i = 0; i < mFonts.size(); i++) { + mFonts.get(i).writeToParcel(out, flag); + } + out.writeString(mLanguage); + out.writeString(mVariant); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Family> CREATOR = new Creator<Family>() { + @Override + public Family createFromParcel(Parcel in) { + return new Family(in); + } + + @Override + public Family[] newArray(int size) { + return new Family[size]; + } + }; + } +} diff --git a/core/java/android/text/FontManager.java b/core/java/android/text/FontManager.java new file mode 100644 index 000000000000..b61cbf3018ef --- /dev/null +++ b/core/java/android/text/FontManager.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.os.RemoteException; + +import com.android.internal.font.IFontManager; + +/** + * Interact with the Font service. + */ +public final class FontManager { + private static final String TAG = "FontManager"; + + private final IFontManager mService; + + /** + * @hide + */ + public FontManager(IFontManager service) { + mService = service; + } + + /** + * Retrieve the system fonts data. This loads the fonts.xml data if needed and loads all system + * fonts in to memory, providing file descriptors for them. + */ + public FontConfig getSystemFonts() { + try { + return mService.getSystemFonts(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java index 79d16fbfaee8..92c70bde5bb2 100644 --- a/core/java/android/util/EventLog.java +++ b/core/java/android/util/EventLog.java @@ -16,6 +16,8 @@ package android.util; +import android.annotation.SystemApi; + import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; @@ -253,6 +255,19 @@ public class EventLog { throws IOException; /** + * Read events from the log, filtered by type, blocking until logs are about to be overwritten. + * @param tags to search for + * @param timestamp timestamp allow logs before this time to be overwritten. + * @param output container to add events into + * @throws IOException if something goes wrong reading events + * @hide + */ + @SystemApi + public static native void readEventsOnWrapping(int[] tags, long timestamp, + Collection<Event> output) + throws IOException; + + /** * Get the name associated with an event type tag code. * @param tag code to look up * @return the name of the tag, or null if no tag has that number diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index b37ea8ebeaa0..105cc47c88aa 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -292,7 +292,7 @@ public final class Display { public static final int STATE_VR = 5; /* The color mode constants defined below must be kept in sync with the ones in - * system/graphics.h */ + * system/core/include/system/graphics-base.h */ /** * Display color mode: The current color mode is unknown or invalid. @@ -306,11 +306,24 @@ public final class Display { */ public static final int COLOR_MODE_DEFAULT = 0; - /** - * Display color mode: SRGB - * @hide - */ + /** @hide */ + public static final int COLOR_MODE_BT601_625 = 1; + /** @hide */ + public static final int COLOR_MODE_BT601_625_UNADJUSTED = 2; + /** @hide */ + public static final int COLOR_MODE_BT601_525 = 3; + /** @hide */ + public static final int COLOR_MODE_BT601_525_UNADJUSTED = 4; + /** @hide */ + public static final int COLOR_MODE_BT709 = 5; + /** @hide */ + public static final int COLOR_MODE_DCI_P3 = 6; + /** @hide */ public static final int COLOR_MODE_SRGB = 7; + /** @hide */ + public static final int COLOR_MODE_ADOBE_RGB = 8; + /** @hide */ + public static final int COLOR_MODE_DISPLAY_P3 = 9; /** * Internal method to create a display. @@ -745,6 +758,8 @@ public final class Display { /** * Returns the display's HDR capabilities. + * + * @see #isHdr() */ public HdrCapabilities getHdrCapabilities() { synchronized (this) { @@ -754,6 +769,35 @@ public final class Display { } /** + * Returns whether this display supports any HDR type. + * + * @see #getHdrCapabilities() + * @see HdrCapabilities#getSupportedHdrTypes() + */ + public boolean isHdr() { + synchronized (this) { + updateDisplayInfoLocked(); + int[] types = mDisplayInfo.hdrCapabilities.getSupportedHdrTypes(); + return types != null && types.length > 0; + } + } + + /** + * Returns whether this display can be used to display wide color gamut content. + */ + public boolean isWideColorGamut() { + synchronized (this) { + updateDisplayInfoLocked(); + for (int colorMode : mDisplayInfo.supportedColorModes) { + if (colorMode == COLOR_MODE_DCI_P3 || colorMode > COLOR_MODE_SRGB) { + return true; + } + } + return false; + } + } + + /** * Gets the supported color modes of this device. * @hide */ diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index a07a7ef600bf..41a13cf59bd8 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -16,16 +16,12 @@ package android.view; -import static android.view.View.KEYBOARD_NAVIGATION_GROUP_CLUSTER; -import static android.view.View.KEYBOARD_NAVIGATION_GROUP_SECTION; - import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.util.ArrayMap; import android.util.SparseArray; import android.util.SparseBooleanArray; -import android.view.View.KeyboardNavigationGroupType; import java.util.ArrayList; import java.util.Collections; @@ -110,31 +106,28 @@ public class FocusFinder { } /** - * Find the root of the next keyboard navigation group after the current one. The group type can - * be either a cluster or a section. - * @param groupType Type of the keyboard navigation group + * Find the root of the next keyboard navigation cluster after the current one. * @param root The view tree to look inside. Cannot be null - * @param currentGroup The starting point of the search. Null means the default group + * @param currentCluster The starting point of the search. Null means the default cluster * @param direction Direction to look - * @return The next group, or null if none exists + * @return The next cluster, or null if none exists */ - public View findNextKeyboardNavigationGroup( - @KeyboardNavigationGroupType int groupType, + public View findNextKeyboardNavigationCluster( @NonNull View root, - @Nullable View currentGroup, + @Nullable View currentCluster, int direction) { View next = null; - final ArrayList<View> groups = mTempList; + final ArrayList<View> clusters = mTempList; try { - groups.clear(); - root.addKeyboardNavigationGroups(groupType, groups, direction); - if (!groups.isEmpty()) { - next = findNextKeyboardNavigationGroup( - groupType, root, currentGroup, groups, direction); + clusters.clear(); + root.addKeyboardNavigationClusters(clusters, direction); + if (!clusters.isEmpty()) { + next = findNextKeyboardNavigationCluster( + root, currentCluster, clusters, direction); } } finally { - groups.clear(); + clusters.clear(); } return next; } @@ -207,25 +200,22 @@ public class FocusFinder { } } - private View findNextKeyboardNavigationGroup( - @KeyboardNavigationGroupType int groupType, + private View findNextKeyboardNavigationCluster( View root, - View currentGroup, - List<View> groups, + View currentCluster, + List<View> clusters, int direction) { - final int count = groups.size(); + final int count = clusters.size(); switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_DOWN: case View.FOCUS_RIGHT: - return getNextKeyboardNavigationGroup( - groupType, root, currentGroup, groups, count); + return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count); case View.FOCUS_BACKWARD: case View.FOCUS_UP: case View.FOCUS_LEFT: - return getPreviousKeyboardNavigationGroup( - groupType, root, currentGroup, groups, count); + return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count); default: throw new IllegalArgumentException("Unknown direction: " + direction); } @@ -331,70 +321,50 @@ public class FocusFinder { return null; } - private static View getNextKeyboardNavigationGroup( - @KeyboardNavigationGroupType int groupType, + private static View getNextKeyboardNavigationCluster( View root, - View currentGroup, - List<View> groups, + View currentCluster, + List<View> clusters, int count) { - if (currentGroup == null) { - // The current group is the default one. - // The next group after the default one is the first one. - // Note that the caller guarantees that 'group' is not empty. - return groups.get(0); + if (currentCluster == null) { + // The current cluster is the default one. + // The next cluster after the default one is the first one. + // Note that the caller guarantees that 'clusters' is not empty. + return clusters.get(0); } - final int position = groups.lastIndexOf(currentGroup); + final int position = clusters.lastIndexOf(currentCluster); if (position >= 0 && position + 1 < count) { - // Return the next non-default group if we can find it. - return groups.get(position + 1); - } - - switch (groupType) { - case KEYBOARD_NAVIGATION_GROUP_CLUSTER: - // The current cluster is the last one. The next one is the default one, i.e. the - // root. - return root; - case KEYBOARD_NAVIGATION_GROUP_SECTION: - // There is no "default section", hence returning the first one. - return groups.get(0); - default: - throw new IllegalArgumentException( - "Unknown keyboard navigation group type: " + groupType); + // Return the next non-default cluster if we can find it. + return clusters.get(position + 1); } + + // The current cluster is the last one. The next one is the default one, i.e. the + // root. + return root; } - private static View getPreviousKeyboardNavigationGroup( - @KeyboardNavigationGroupType int groupType, + private static View getPreviousKeyboardNavigationCluster( View root, - View currentGroup, - List<View> groups, + View currentCluster, + List<View> clusters, int count) { - if (currentGroup == null) { - // The current group is the default one. - // The previous group before the default one is the last one. - // Note that the caller guarantees that 'groups' is not empty. - return groups.get(count - 1); + if (currentCluster == null) { + // The current cluster is the default one. + // The previous cluster before the default one is the last one. + // Note that the caller guarantees that 'clusters' is not empty. + return clusters.get(count - 1); } - final int position = groups.indexOf(currentGroup); + final int position = clusters.indexOf(currentCluster); if (position > 0) { - // Return the previous non-default group if we can find it. - return groups.get(position - 1); - } - - switch (groupType) { - case KEYBOARD_NAVIGATION_GROUP_CLUSTER: - // The current cluster is the first one. The previous one is the default one, i.e. - // the root. - return root; - case KEYBOARD_NAVIGATION_GROUP_SECTION: - // There is no "default section", hence returning the last one. - return groups.get(count - 1); - default: - throw new IllegalArgumentException( - "Unknown keyboard navigation group type: " + groupType); + // Return the previous non-default cluster if we can find it. + return clusters.get(position - 1); } + + // The current cluster is the first one. The previous one is the default one, i.e. + // the root. + return root; } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 26e311c6a921..13555f47c2c5 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1252,14 +1252,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Retention(RetentionPolicy.SOURCE) public @interface FocusRealDirection {} // Like @FocusDirection, but without forward/backward - /** @hide */ - @IntDef({ - KEYBOARD_NAVIGATION_GROUP_CLUSTER, - KEYBOARD_NAVIGATION_GROUP_SECTION - }) - @Retention(RetentionPolicy.SOURCE) - public @interface KeyboardNavigationGroupType {} - /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. @@ -1293,18 +1285,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int FOCUS_DOWN = 0x00000082; /** - * Use with {@link #keyboardNavigationGroupSearch(int, View, int)}. Search for a keyboard - * navigation cluster. - */ - public static final int KEYBOARD_NAVIGATION_GROUP_CLUSTER = 1; - - /** - * Use with {@link #keyboardNavigationGroupSearch(int, View, int)}. Search for a keyboard - * navigation section. - */ - public static final int KEYBOARD_NAVIGATION_GROUP_SECTION = 2; - - /** * Bits of {@link #getMeasuredWidthAndState()} and * {@link #getMeasuredWidthAndState()} that provide the actual measured size. */ @@ -2500,7 +2480,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG3_SCROLL_INDICATOR_END * 1 PFLAG3_ASSIST_BLOCKED * 1 PFLAG3_CLUSTER - * 1 PFLAG3_SECTION + * x * NO LONGER NEEDED, SHOULD BE REUSED * * 1 PFLAG3_FINGER_DOWN * 1 PFLAG3_FOCUSED_BY_DEFAULT * xxxx * NO LONGER NEEDED, SHOULD BE REUSED * @@ -2710,14 +2690,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int PFLAG3_CLUSTER = 0x8000; /** - * Flag indicating that the view is a root of a keyboard navigation section. - * - * @see #isKeyboardNavigationSection() - * @see #setKeyboardNavigationSection(boolean) - */ - private static final int PFLAG3_SECTION = 0x10000; - - /** * Indicates that the user is currently touching the screen. * Currently used for the tooltip positioning only. */ @@ -3807,11 +3779,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ int mNextClusterForwardId = View.NO_ID; - /** - * User-specified next keyboard navigation section. - */ - int mNextSectionForwardId = View.NO_ID; - private CheckForLongPress mPendingCheckForLongPress; private CheckForTap mPendingCheckForTap = null; private PerformClick mPerformClick; @@ -4622,9 +4589,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_nextClusterForward: mNextClusterForwardId = a.getResourceId(attr, View.NO_ID); break; - case R.styleable.View_nextSectionForward: - mNextSectionForwardId = a.getResourceId(attr, View.NO_ID); - break; case R.styleable.View_minWidth: mMinWidth = a.getDimensionPixelSize(attr, 0); break; @@ -4769,11 +4733,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setKeyboardNavigationCluster(a.getBoolean(attr, true)); } break; - case R.styleable.View_keyboardNavigationSection: - if (a.peekValue(attr) != null) { - setKeyboardNavigationSection(a.getBoolean(attr, true)); - } - break; case R.styleable.View_focusedByDefault: if (a.peekValue(attr) != null) { setFocusedByDefault(a.getBoolean(attr, true)); @@ -8043,28 +8002,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Gets the id of the root of the next keyboard navigation section. - * @return The next keyboard navigation section ID, or {@link #NO_ID} if the framework should - * decide automatically. - * - * @attr ref android.R.styleable#View_nextSectionForward - */ - public int getNextSectionForwardId() { - return mNextSectionForwardId; - } - - /** - * Sets the id of the view to use as the root of the next keyboard navigation section. - * @param nextSectionForwardId The next section ID, or {@link #NO_ID} if the framework should - * decide automatically. - * - * @attr ref android.R.styleable#View_nextSectionForward - */ - public void setNextSectionForwardId(int nextSectionForwardId) { - mNextSectionForwardId = nextSectionForwardId; - } - - /** * Returns the visibility of this view and all of its ancestors * * @return True if this view and all of its ancestors are {@link #VISIBLE} @@ -9186,49 +9123,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns whether this View is a root of a keyboard navigation section. - * - * @return True if this view is a root of a section, or false otherwise. - * @attr ref android.R.styleable#View_keyboardNavigationSection - */ - @ViewDebug.ExportedProperty(category = "keyboardNavigationSection") - public final boolean isKeyboardNavigationSection() { - return (mPrivateFlags3 & PFLAG3_SECTION) != 0; - } - - /** - * Set whether this view is a root of a keyboard navigation section. - * - * @param isSection If true, this view is a root of a section. - * - * @attr ref android.R.styleable#View_keyboardNavigationSection - */ - public void setKeyboardNavigationSection(boolean isSection) { - if (isSection) { - mPrivateFlags3 |= PFLAG3_SECTION; - } else { - mPrivateFlags3 &= ~PFLAG3_SECTION; - } - } - - final boolean isKeyboardNavigationGroupOfType(@KeyboardNavigationGroupType int groupType) { - switch (groupType) { - case KEYBOARD_NAVIGATION_GROUP_CLUSTER: - return isKeyboardNavigationCluster(); - case KEYBOARD_NAVIGATION_GROUP_SECTION: - return isKeyboardNavigationSection(); - default: - throw new IllegalArgumentException( - "Unknown keyboard navigation group type: " + groupType); - } - } - - /** * Returns whether this View should receive focus when the focus is restored for the view * hierarchy containing this view. * <p> * Focus gets restored for a view hierarchy when the root of the hierarchy gets added to a - * window or serves as a target of cluster or section navigation. + * window or serves as a target of cluster navigation. * * @see #restoreDefaultFocus(int) * @@ -9245,7 +9144,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * hierarchy containing this view. * <p> * Focus gets restored for a view hierarchy when the root of the hierarchy gets added to a - * window or serves as a target of cluster or section navigation. + * window or serves as a target of cluster navigation. * * @param isFocusedByDefault {@code true} to set this view as the default-focus view, * {@code false} otherwise. @@ -9284,35 +9183,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Find the nearest keyboard navigation group in the specified direction. The group type can be - * either a cluster or a section. - * This does not actually give focus to that group. + * Find the nearest keyboard navigation cluster in the specified direction. + * This does not actually give focus to that cluster. * - * @param groupType Type of the keyboard navigation group - * @param currentGroup The starting point of the search. Null means the current group is not - * found yet + * @param currentCluster The starting point of the search. Null means the current cluster is not + * found yet * @param direction Direction to look * - * @return The nearest keyboard navigation group in the specified direction, or null if none + * @return The nearest keyboard navigation cluster in the specified direction, or null if none * can be found */ - public View keyboardNavigationGroupSearch( - @KeyboardNavigationGroupType int groupType, View currentGroup, int direction) { - if (isKeyboardNavigationGroupOfType(groupType)) { - currentGroup = this; + public View keyboardNavigationClusterSearch(View currentCluster, int direction) { + if (isKeyboardNavigationCluster()) { + currentCluster = this; } - if (isRootNamespace() - || (groupType == KEYBOARD_NAVIGATION_GROUP_SECTION - && isKeyboardNavigationCluster())) { + if (isRootNamespace()) { // Root namespace means we should consider ourselves the top of the // tree for group searching; otherwise we could be group searching // into other tabs. see LocalActivityManager and TabHost for more info. - // In addition, a cluster node works as a root for section searches. - return FocusFinder.getInstance().findNextKeyboardNavigationGroup( - groupType, this, currentGroup, direction); + return FocusFinder.getInstance().findNextKeyboardNavigationCluster( + this, currentCluster, direction); } else if (mParent != null) { - return mParent.keyboardNavigationGroupSearch( - groupType, currentGroup, direction); + return mParent.keyboardNavigationClusterSearch(currentCluster, direction); } return null; } @@ -9440,19 +9332,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Adds any keyboard navigation group roots that are descendants of this view (possibly - * including this view if it is a group root itself) to views. The group type can be either a - * cluster or a section. + * Adds any keyboard navigation cluster roots that are descendants of this view (possibly + * including this view if it is a cluster root itself) to views. * - * @param groupType Type of the keyboard navigation group - * @param views Keyboard navigation group roots found so far + * @param views Keyboard navigation cluster roots found so far * @param direction Direction to look */ - public void addKeyboardNavigationGroups( - @KeyboardNavigationGroupType int groupType, + public void addKeyboardNavigationClusters( @NonNull Collection<View> views, int direction) { - if (!(isKeyboardNavigationGroupOfType(groupType))) { + if (!(isKeyboardNavigationCluster())) { return; } views.add(this); diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index d252d75856da..480741eb988a 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -915,13 +915,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public View focusSearch(View focused, int direction) { - if (isRootNamespace() - || isKeyboardNavigationCluster() - && (direction == FOCUS_FORWARD || direction == FOCUS_BACKWARD)) { + if (isRootNamespace()) { // root namespace means we should consider ourselves the top of the // tree for focus searching; otherwise we could be focus searching // into other tabs. see LocalActivityManager and TabHost for more info. - // Cluster's root works same way for the forward and backward navigation. return FocusFinder.getInstance().findNextFocus(this, focused, direction); } else if (mParent != null) { return mParent.focusSearch(focused, direction); @@ -1136,12 +1133,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { - if (isKeyboardNavigationCluster() - && (direction == FOCUS_FORWARD || direction == FOCUS_BACKWARD) && !hasFocus()) { - // A cluster cannot be focus-entered from outside using forward/backward navigation. - return; - } - final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); @@ -1175,11 +1166,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - public void addKeyboardNavigationGroups( - @KeyboardNavigationGroupType int groupType, Collection<View> views, int direction) { + public void addKeyboardNavigationClusters(Collection<View> views, int direction) { final int focusableCount = views.size(); - super.addKeyboardNavigationGroups(groupType, views, direction); + super.addKeyboardNavigationClusters(views, direction); if (focusableCount != views.size()) { // No need to look for groups inside a group. @@ -1195,14 +1185,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { final View child = children[i]; - if (groupType == KEYBOARD_NAVIGATION_GROUP_SECTION - && child.isKeyboardNavigationCluster()) { - // When the current cluster is the default cluster, and we are searching for - // sections, ignore sections inside non-default clusters. - continue; - } if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - child.addKeyboardNavigationGroups(groupType, views, direction); + child.addKeyboardNavigationClusters(views, direction); } } } @@ -3072,8 +3056,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View[] children = mChildren; for (int i = index; i != end; i += increment) { View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE - && !child.isKeyboardNavigationCluster()) { + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index c9277ca0bfa1..79b05cdb6e50 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -18,7 +18,6 @@ package android.view; import android.graphics.Rect; import android.os.Bundle; -import android.view.View.KeyboardNavigationGroupType; import android.view.accessibility.AccessibilityEvent; /** @@ -148,20 +147,17 @@ public interface ViewParent { public View focusSearch(View v, int direction); /** - * Find the nearest keyboard navigation group in the specified direction. The group type can be - * either a cluster or a section. - * This does not actually give focus to that group. + * Find the nearest keyboard navigation cluster in the specified direction. + * This does not actually give focus to that cluster. * - * @param groupType Type of the keyboard navigation group - * @param currentGroup The starting point of the search. Null means the current group is not - * found yet + * @param currentCluster The starting point of the search. Null means the current cluster is not + * found yet * @param direction Direction to look * - * @return The nearest keyboard navigation group in the specified direction, or null if none + * @return The nearest keyboard navigation cluster in the specified direction, or null if none * can be found */ - View keyboardNavigationGroupSearch( - @KeyboardNavigationGroupType int groupType, View currentGroup, int direction); + View keyboardNavigationClusterSearch(View currentCluster, int direction); /** * Change the z order of the child so it's on top of all other children. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c0f2c377ee88..3cbe82e8b6b9 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -16,8 +16,6 @@ package android.view; -import static android.view.View.KEYBOARD_NAVIGATION_GROUP_CLUSTER; -import static android.view.View.KEYBOARD_NAVIGATION_GROUP_SECTION; import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY; @@ -73,7 +71,6 @@ import android.util.TimeUtils; import android.util.TypedValue; import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; -import android.view.View.KeyboardNavigationGroupType; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -4397,14 +4394,13 @@ public final class ViewRootImpl implements ViewParent, return false; } - private boolean performKeyboardGroupNavigation( - @KeyboardNavigationGroupType int groupType, int direction) { + private boolean performKeyboardGroupNavigation(int direction) { final View focused = mView.findFocus(); - final View group = focused != null - ? focused.keyboardNavigationGroupSearch(groupType, null, direction) - : keyboardNavigationGroupSearch(groupType, null, direction); + final View cluster = focused != null + ? focused.keyboardNavigationClusterSearch(null, direction) + : keyboardNavigationClusterSearch(null, direction); - if (group != null && group.restoreDefaultFocus(View.FOCUS_DOWN)) { + if (cluster != null && cluster.restoreDefaultFocus(View.FOCUS_DOWN)) { return true; } @@ -4424,32 +4420,15 @@ public final class ViewRootImpl implements ViewParent, } int groupNavigationDirection = 0; - @KeyboardNavigationGroupType int groupType = 0; if (event.getAction() == KeyEvent.ACTION_DOWN && event.isCtrlPressed()) { final int character = event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_CTRL_MASK); if (character == '+') { - groupType = KEYBOARD_NAVIGATION_GROUP_CLUSTER; groupNavigationDirection = View.FOCUS_FORWARD; } if (character == '_') { - groupType = KEYBOARD_NAVIGATION_GROUP_CLUSTER; - groupNavigationDirection = View.FOCUS_BACKWARD; - } - } - - if (event.getAction() == KeyEvent.ACTION_DOWN && event.isAltPressed()) { - final int character = - event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_ALT_MASK); - if (character == '+') { - groupType = KEYBOARD_NAVIGATION_GROUP_SECTION; - groupNavigationDirection = View.FOCUS_FORWARD; - } - - if (character == '_') { - groupType = KEYBOARD_NAVIGATION_GROUP_SECTION; groupNavigationDirection = View.FOCUS_BACKWARD; } } @@ -4479,7 +4458,7 @@ public final class ViewRootImpl implements ViewParent, // Handle automatic focus changes. if (event.getAction() == KeyEvent.ACTION_DOWN) { if (groupNavigationDirection != 0) { - if (performKeyboardGroupNavigation(groupType, groupNavigationDirection)) { + if (performKeyboardGroupNavigation(groupNavigationDirection)) { return FINISH_HANDLED; } } else { @@ -5910,11 +5889,10 @@ public final class ViewRootImpl implements ViewParent, * {@inheritDoc} */ @Override - public View keyboardNavigationGroupSearch( - @KeyboardNavigationGroupType int groupType, View currentGroup, int direction) { + public View keyboardNavigationClusterSearch(View currentCluster, int direction) { checkThread(); - return FocusFinder.getInstance().findNextKeyboardNavigationGroup(groupType, - mView, currentGroup, direction); + return FocusFinder.getInstance().findNextKeyboardNavigationCluster( + mView, currentCluster, direction); } public void debug() { diff --git a/core/java/android/webkit/UserPackage.java b/core/java/android/webkit/UserPackage.java new file mode 100644 index 000000000000..404bcf453aaf --- /dev/null +++ b/core/java/android/webkit/UserPackage.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; +import android.os.UserManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for storing a (user,PackageInfo) mapping. + * @hide + */ +public class UserPackage { + private final UserInfo mUserInfo; + private final PackageInfo mPackageInfo; + + public UserPackage(UserInfo user, PackageInfo packageInfo) { + this.mUserInfo = user; + this.mPackageInfo = packageInfo; + } + + /** + * Returns a list of (User,PackageInfo) pairs corresponding to the PackageInfos for all + * device users for the package named {@param packageName}. + */ + public static List<UserPackage> getPackageInfosAllUsers(Context context, + String packageName, int packageFlags) { + List<UserInfo> users = getAllUsers(context); + List<UserPackage> userPackages = new ArrayList<UserPackage>(users.size()); + for (UserInfo user : users) { + PackageInfo packageInfo = null; + try { + packageInfo = context.getPackageManager().getPackageInfoAsUser( + packageName, packageFlags, user.id); + } catch (NameNotFoundException e) { + } + userPackages.add(new UserPackage(user, packageInfo)); + } + return userPackages; + } + + /** + * Returns whether the given package is enabled. + * This state can be changed by the user from Settings->Apps + */ + public boolean isEnabledPackage() { + if (mPackageInfo == null) return false; + return mPackageInfo.applicationInfo.enabled; + } + + /** + * Return true if the package is installed and not hidden + */ + public boolean isInstalledPackage() { + if (mPackageInfo == null) return false; + return (((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) + && ((mPackageInfo.applicationInfo.privateFlags + & ApplicationInfo.PRIVATE_FLAG_HIDDEN) == 0)); + } + + public UserInfo getUserInfo() { + return mUserInfo; + } + + public PackageInfo getPackageInfo() { + return mPackageInfo; + } + + + private static List<UserInfo> getAllUsers(Context context) { + UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + return userManager.getUsers(false); + } + +} diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 2cdff7906462..92d0d7141370 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.graphics.Canvas; +import android.os.RemoteException; import android.os.SystemProperties; import android.os.Trace; import android.util.SparseArray; @@ -206,4 +207,15 @@ public final class WebViewDelegate { appInfo.getBaseResourcePath(), newAssetPath); } } + + /** + * Returns whether WebView should run in multiprocess mode. + */ + public boolean isMultiProcessEnabled() { + try { + return WebViewFactory.getUpdateService().isMultiProcessEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index d734d17b2dc9..ab1d9b9c6f6e 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -749,10 +749,7 @@ public class ResolverActivity extends Activity { } } else { try { - AppGlobals.getPackageManager().setLastChosenActivity(intent, - intent.resolveType(getContentResolver()), - PackageManager.MATCH_DEFAULT_ONLY, - filter, bestMatch, intent.getComponent()); + mAdapter.mResolverListController.setLastChosen(intent, filter, bestMatch); } catch (RemoteException re) { Log.d(TAG, "Error calling setLastChosenActivity\n" + re); } @@ -1312,10 +1309,7 @@ public class ResolverActivity extends Activity { protected boolean rebuildList() { List<ResolvedComponentInfo> currentResolveList = null; try { - final Intent primaryIntent = getTargetIntent(); - mLastChosen = AppGlobals.getPackageManager().getLastChosenActivity( - primaryIntent, primaryIntent.resolveTypeIfNeeded(getContentResolver()), - PackageManager.MATCH_DEFAULT_ONLY); + mLastChosen = mResolverListController.getLastChosen(); } catch (RemoteException re) { Log.d(TAG, "Error calling getLastChosenActivity\n" + re); } diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index f88f6f9a07e0..00faf65d004c 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -19,13 +19,16 @@ package com.android.internal.app; import android.annotation.WorkerThread; import android.app.ActivityManager; +import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.RemoteException; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -66,6 +69,22 @@ public class ResolverListController { } @VisibleForTesting + ResolveInfo getLastChosen() throws RemoteException { + return AppGlobals.getPackageManager().getLastChosenActivity( + mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY); + } + + @VisibleForTesting + void setLastChosen(Intent intent, IntentFilter filter, int match) + throws RemoteException { + AppGlobals.getPackageManager().setLastChosenActivity(intent, + intent.resolveType(mContext.getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY, + filter, match, intent.getComponent()); + } + + @VisibleForTesting public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent( boolean shouldGetResolvedFilter, boolean shouldGetActivityMetadata, diff --git a/core/java/com/android/internal/font/IFontManager.aidl b/core/java/com/android/internal/font/IFontManager.aidl new file mode 100644 index 000000000000..52a626288caf --- /dev/null +++ b/core/java/com/android/internal/font/IFontManager.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.font; + +import android.text.FontConfig; + +/** + * Interface to the font manager. + * @hide + */ +interface IFontManager { + FontConfig getSystemFonts(); +} diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index 716997f815dc..c08cd7284876 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -1080,22 +1080,6 @@ public class InputMethodUtils { return enabledSubtypes; } - // At the initial boot, the settings for input methods are not set, - // so we need to enable IME in that case. - public void enableAllIMEsIfThereIsNoEnabledIME() { - if (TextUtils.isEmpty(getEnabledInputMethodsStr())) { - StringBuilder sb = new StringBuilder(); - final int N = mMethodList.size(); - for (int i = 0; i < N; i++) { - InputMethodInfo imi = mMethodList.get(i); - Slog.i(TAG, "Adding: " + imi.getId()); - if (i > 0) sb.append(':'); - sb.append(imi.getId()); - } - putEnabledInputMethodsStr(sb.toString()); - } - } - public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(), mInputMethodSplitter, diff --git a/core/java/com/android/internal/logging/legacy/EventLogCollector.java b/core/java/com/android/internal/logging/legacy/EventLogCollector.java index 952ae2386abc..eba7d0f5f532 100644 --- a/core/java/com/android/internal/logging/legacy/EventLogCollector.java +++ b/core/java/com/android/internal/logging/legacy/EventLogCollector.java @@ -178,7 +178,7 @@ public class EventLogCollector { public void readEvents(int[] tags, Collection<Event> events) throws IOException { // Testing in Android: the Static Final Class Strikes Back! ArrayList<EventLog.Event> nativeEvents = new ArrayList<>(); - EventLog.readEvents(tags, nativeEvents); + EventLog.readEventsOnWrapping(tags, 0L, nativeEvents); for (EventLog.Event nativeEvent : nativeEvents) { Event event = new Event(nativeEvent); events.add(event); diff --git a/core/java/com/android/internal/widget/AdapterHelper.java b/core/java/com/android/internal/widget/AdapterHelper.java new file mode 100644 index 000000000000..f47d43075d38 --- /dev/null +++ b/core/java/com/android/internal/widget/AdapterHelper.java @@ -0,0 +1,775 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.util.Log; +import android.util.Pools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper class that can enqueue and process adapter update operations. + * <p> + * To support animations, RecyclerView presents an older version the Adapter to best represent + * previous state of the layout. Sometimes, this is not trivial when items are removed that were + * not laid out, in which case, RecyclerView has no way of providing that item's view for + * animations. + * <p> + * AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During + * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass + * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them + * according to previously deferred operation and dispatch them before the first layout pass. It + * also takes care of updating deferred UpdateOps since order of operations is changed by this + * process. + * <p> + * Although operations may be forwarded to LayoutManager in different orders, resulting data set + * is guaranteed to be the consistent. + */ +class AdapterHelper implements OpReorderer.Callback { + + static final int POSITION_TYPE_INVISIBLE = 0; + + static final int POSITION_TYPE_NEW_OR_LAID_OUT = 1; + + private static final boolean DEBUG = false; + + private static final String TAG = "AHT"; + + private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE); + + final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>(); + + final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>(); + + final Callback mCallback; + + Runnable mOnItemProcessedCallback; + + final boolean mDisableRecycler; + + final OpReorderer mOpReorderer; + + private int mExistingUpdateTypes = 0; + + AdapterHelper(Callback callback) { + this(callback, false); + } + + AdapterHelper(Callback callback, boolean disableRecycler) { + mCallback = callback; + mDisableRecycler = disableRecycler; + mOpReorderer = new OpReorderer(this); + } + + AdapterHelper addUpdateOp(UpdateOp... ops) { + Collections.addAll(mPendingUpdates, ops); + return this; + } + + void reset() { + recycleUpdateOpsAndClearList(mPendingUpdates); + recycleUpdateOpsAndClearList(mPostponedList); + mExistingUpdateTypes = 0; + } + + void preProcess() { + mOpReorderer.reorderOps(mPendingUpdates); + final int count = mPendingUpdates.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + applyAdd(op); + break; + case UpdateOp.REMOVE: + applyRemove(op); + break; + case UpdateOp.UPDATE: + applyUpdate(op); + break; + case UpdateOp.MOVE: + applyMove(op); + break; + } + if (mOnItemProcessedCallback != null) { + mOnItemProcessedCallback.run(); + } + } + mPendingUpdates.clear(); + } + + void consumePostponedUpdates() { + final int count = mPostponedList.size(); + for (int i = 0; i < count; i++) { + mCallback.onDispatchSecondPass(mPostponedList.get(i)); + } + recycleUpdateOpsAndClearList(mPostponedList); + mExistingUpdateTypes = 0; + } + + private void applyMove(UpdateOp op) { + // MOVE ops are pre-processed so at this point, we know that item is still in the adapter. + // otherwise, it would be converted into a REMOVE operation + postponeAndUpdateViewHolders(op); + } + + private void applyRemove(UpdateOp op) { + int tmpStart = op.positionStart; + int tmpCount = 0; + int tmpEnd = op.positionStart + op.itemCount; + int type = -1; + for (int position = op.positionStart; position < tmpEnd; position++) { + boolean typeChanged = false; + RecyclerView.ViewHolder vh = mCallback.findViewHolder(position); + if (vh != null || canFindInPreLayout(position)) { + // If a ViewHolder exists or this is a newly added item, we can defer this update + // to post layout stage. + // * For existing ViewHolders, we'll fake its existence in the pre-layout phase. + // * For items that are added and removed in the same process cycle, they won't + // have any effect in pre-layout since their add ops are already deferred to + // post-layout pass. + if (type == POSITION_TYPE_INVISIBLE) { + // Looks like we have other updates that we cannot merge with this one. + // Create an UpdateOp and dispatch it to LayoutManager. + UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null); + dispatchAndUpdateViewHolders(newOp); + typeChanged = true; + } + type = POSITION_TYPE_NEW_OR_LAID_OUT; + } else { + // This update cannot be recovered because we don't have a ViewHolder representing + // this position. Instead, post it to LayoutManager immediately + if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { + // Looks like we have other updates that we cannot merge with this one. + // Create UpdateOp op and dispatch it to LayoutManager. + UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null); + postponeAndUpdateViewHolders(newOp); + typeChanged = true; + } + type = POSITION_TYPE_INVISIBLE; + } + if (typeChanged) { + position -= tmpCount; // also equal to tmpStart + tmpEnd -= tmpCount; + tmpCount = 1; + } else { + tmpCount++; + } + } + if (tmpCount != op.itemCount) { // all 1 effect + recycleUpdateOp(op); + op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null); + } + if (type == POSITION_TYPE_INVISIBLE) { + dispatchAndUpdateViewHolders(op); + } else { + postponeAndUpdateViewHolders(op); + } + } + + private void applyUpdate(UpdateOp op) { + int tmpStart = op.positionStart; + int tmpCount = 0; + int tmpEnd = op.positionStart + op.itemCount; + int type = -1; + for (int position = op.positionStart; position < tmpEnd; position++) { + RecyclerView.ViewHolder vh = mCallback.findViewHolder(position); + if (vh != null || canFindInPreLayout(position)) { // deferred + if (type == POSITION_TYPE_INVISIBLE) { + UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, + op.payload); + dispatchAndUpdateViewHolders(newOp); + tmpCount = 0; + tmpStart = position; + } + type = POSITION_TYPE_NEW_OR_LAID_OUT; + } else { // applied + if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { + UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, + op.payload); + postponeAndUpdateViewHolders(newOp); + tmpCount = 0; + tmpStart = position; + } + type = POSITION_TYPE_INVISIBLE; + } + tmpCount++; + } + if (tmpCount != op.itemCount) { // all 1 effect + Object payload = op.payload; + recycleUpdateOp(op); + op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload); + } + if (type == POSITION_TYPE_INVISIBLE) { + dispatchAndUpdateViewHolders(op); + } else { + postponeAndUpdateViewHolders(op); + } + } + + private void dispatchAndUpdateViewHolders(UpdateOp op) { + // tricky part. + // traverse all postpones and revert their changes on this op if necessary, apply updated + // dispatch to them since now they are after this op. + if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) { + throw new IllegalArgumentException("should not dispatch add or move for pre layout"); + } + if (DEBUG) { + Log.d(TAG, "dispatch (pre)" + op); + Log.d(TAG, "postponed state before:"); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + + // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial + // TODO Since move ops are pushed to end, we should not need this anymore + int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd); + if (DEBUG) { + Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart); + } + int tmpCnt = 1; + int offsetPositionForPartial = op.positionStart; + final int positionMultiplier; + switch (op.cmd) { + case UpdateOp.UPDATE: + positionMultiplier = 1; + break; + case UpdateOp.REMOVE: + positionMultiplier = 0; + break; + default: + throw new IllegalArgumentException("op should be remove or update." + op); + } + for (int p = 1; p < op.itemCount; p++) { + final int pos = op.positionStart + (positionMultiplier * p); + int updatedPos = updatePositionWithPostponed(pos, op.cmd); + if (DEBUG) { + Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos); + } + boolean continuous = false; + switch (op.cmd) { + case UpdateOp.UPDATE: + continuous = updatedPos == tmpStart + 1; + break; + case UpdateOp.REMOVE: + continuous = updatedPos == tmpStart; + break; + } + if (continuous) { + tmpCnt++; + } else { + // need to dispatch this separately + UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload); + if (DEBUG) { + Log.d(TAG, "need to dispatch separately " + tmp); + } + dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); + recycleUpdateOp(tmp); + if (op.cmd == UpdateOp.UPDATE) { + offsetPositionForPartial += tmpCnt; + } + tmpStart = updatedPos; // need to remove previously dispatched + tmpCnt = 1; + } + } + Object payload = op.payload; + recycleUpdateOp(op); + if (tmpCnt > 0) { + UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload); + if (DEBUG) { + Log.d(TAG, "dispatching:" + tmp); + } + dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); + recycleUpdateOp(tmp); + } + if (DEBUG) { + Log.d(TAG, "post dispatch"); + Log.d(TAG, "postponed state after:"); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + } + + void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) { + mCallback.onDispatchFirstPass(op); + switch (op.cmd) { + case UpdateOp.REMOVE: + mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload); + break; + default: + throw new IllegalArgumentException("only remove and update ops can be dispatched" + + " in first pass"); + } + } + + private int updatePositionWithPostponed(int pos, int cmd) { + final int count = mPostponedList.size(); + for (int i = count - 1; i >= 0; i--) { + UpdateOp postponed = mPostponedList.get(i); + if (postponed.cmd == UpdateOp.MOVE) { + int start, end; + if (postponed.positionStart < postponed.itemCount) { + start = postponed.positionStart; + end = postponed.itemCount; + } else { + start = postponed.itemCount; + end = postponed.positionStart; + } + if (pos >= start && pos <= end) { + //i'm affected + if (start == postponed.positionStart) { + if (cmd == UpdateOp.ADD) { + postponed.itemCount++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.itemCount--; + } + // op moved to left, move it right to revert + pos++; + } else { + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + } + // op was moved right, move left to revert + pos--; + } + } else if (pos < postponed.positionStart) { + // postponed MV is outside the dispatched OP. if it is before, offset + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + postponed.itemCount++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + postponed.itemCount--; + } + } + } else { + if (postponed.positionStart <= pos) { + if (postponed.cmd == UpdateOp.ADD) { + pos -= postponed.itemCount; + } else if (postponed.cmd == UpdateOp.REMOVE) { + pos += postponed.itemCount; + } + } else { + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + } + } + } + if (DEBUG) { + Log.d(TAG, "dispath (step" + i + ")"); + Log.d(TAG, "postponed state:" + i + ", pos:" + pos); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + } + for (int i = mPostponedList.size() - 1; i >= 0; i--) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (op.itemCount == op.positionStart || op.itemCount < 0) { + mPostponedList.remove(i); + recycleUpdateOp(op); + } + } else if (op.itemCount <= 0) { + mPostponedList.remove(i); + recycleUpdateOp(op); + } + } + return pos; + } + + private boolean canFindInPreLayout(int position) { + final int count = mPostponedList.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (findPositionOffset(op.itemCount, i + 1) == position) { + return true; + } + } else if (op.cmd == UpdateOp.ADD) { + // TODO optimize. + final int end = op.positionStart + op.itemCount; + for (int pos = op.positionStart; pos < end; pos++) { + if (findPositionOffset(pos, i + 1) == position) { + return true; + } + } + } + } + return false; + } + + private void applyAdd(UpdateOp op) { + postponeAndUpdateViewHolders(op); + } + + private void postponeAndUpdateViewHolders(UpdateOp op) { + if (DEBUG) { + Log.d(TAG, "postponing " + op); + } + mPostponedList.add(op); + switch (op.cmd) { + case UpdateOp.ADD: + mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); + break; + case UpdateOp.MOVE: + mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); + break; + case UpdateOp.REMOVE: + mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, + op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); + break; + default: + throw new IllegalArgumentException("Unknown update op type for " + op); + } + } + + boolean hasPendingUpdates() { + return mPendingUpdates.size() > 0; + } + + boolean hasAnyUpdateTypes(int updateTypes) { + return (mExistingUpdateTypes & updateTypes) != 0; + } + + int findPositionOffset(int position) { + return findPositionOffset(position, 0); + } + + int findPositionOffset(int position, int firstPostponedItem) { + int count = mPostponedList.size(); + for (int i = firstPostponedItem; i < count; ++i) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (op.positionStart == position) { + position = op.itemCount; + } else { + if (op.positionStart < position) { + position--; // like a remove + } + if (op.itemCount <= position) { + position++; // like an add + } + } + } else if (op.positionStart <= position) { + if (op.cmd == UpdateOp.REMOVE) { + if (position < op.positionStart + op.itemCount) { + return -1; + } + position -= op.itemCount; + } else if (op.cmd == UpdateOp.ADD) { + position += op.itemCount; + } + } + } + return position; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload)); + mExistingUpdateTypes |= UpdateOp.UPDATE; + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeInserted(int positionStart, int itemCount) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null)); + mExistingUpdateTypes |= UpdateOp.ADD; + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeRemoved(int positionStart, int itemCount) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null)); + mExistingUpdateTypes |= UpdateOp.REMOVE; + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeMoved(int from, int to, int itemCount) { + if (from == to) { + return false; // no-op + } + if (itemCount != 1) { + throw new IllegalArgumentException("Moving more than 1 item is not supported yet"); + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null)); + mExistingUpdateTypes |= UpdateOp.MOVE; + return mPendingUpdates.size() == 1; + } + + /** + * Skips pre-processing and applies all updates in one pass. + */ + void consumeUpdatesInOnePass() { + // we still consume postponed updates (if there is) in case there was a pre-process call + // w/o a matching consumePostponedUpdates. + consumePostponedUpdates(); + final int count = mPendingUpdates.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); + break; + case UpdateOp.REMOVE: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.onDispatchSecondPass(op); + mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); + break; + case UpdateOp.MOVE: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); + break; + } + if (mOnItemProcessedCallback != null) { + mOnItemProcessedCallback.run(); + } + } + recycleUpdateOpsAndClearList(mPendingUpdates); + mExistingUpdateTypes = 0; + } + + public int applyPendingUpdatesToPosition(int position) { + final int size = mPendingUpdates.size(); + for (int i = 0; i < size; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + if (op.positionStart <= position) { + position += op.itemCount; + } + break; + case UpdateOp.REMOVE: + if (op.positionStart <= position) { + final int end = op.positionStart + op.itemCount; + if (end > position) { + return RecyclerView.NO_POSITION; + } + position -= op.itemCount; + } + break; + case UpdateOp.MOVE: + if (op.positionStart == position) { + position = op.itemCount; //position end + } else { + if (op.positionStart < position) { + position -= 1; + } + if (op.itemCount <= position) { + position += 1; + } + } + break; + } + } + return position; + } + + boolean hasUpdates() { + return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty(); + } + + /** + * Queued operation to happen when child views are updated. + */ + static class UpdateOp { + + static final int ADD = 1; + + static final int REMOVE = 1 << 1; + + static final int UPDATE = 1 << 2; + + static final int MOVE = 1 << 3; + + static final int POOL_SIZE = 30; + + int cmd; + + int positionStart; + + Object payload; + + // holds the target position if this is a MOVE + int itemCount; + + UpdateOp(int cmd, int positionStart, int itemCount, Object payload) { + this.cmd = cmd; + this.positionStart = positionStart; + this.itemCount = itemCount; + this.payload = payload; + } + + String cmdToString() { + switch (cmd) { + case ADD: + return "add"; + case REMOVE: + return "rm"; + case UPDATE: + return "up"; + case MOVE: + return "mv"; + } + return "??"; + } + + @Override + public String toString() { + return Integer.toHexString(System.identityHashCode(this)) + + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + + ",p:" + payload + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UpdateOp op = (UpdateOp) o; + + if (cmd != op.cmd) { + return false; + } + if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) { + // reverse of this is also true + if (itemCount == op.positionStart && positionStart == op.itemCount) { + return true; + } + } + if (itemCount != op.itemCount) { + return false; + } + if (positionStart != op.positionStart) { + return false; + } + if (payload != null) { + if (!payload.equals(op.payload)) { + return false; + } + } else if (op.payload != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = cmd; + result = 31 * result + positionStart; + result = 31 * result + itemCount; + return result; + } + } + + @Override + public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) { + UpdateOp op = mUpdateOpPool.acquire(); + if (op == null) { + op = new UpdateOp(cmd, positionStart, itemCount, payload); + } else { + op.cmd = cmd; + op.positionStart = positionStart; + op.itemCount = itemCount; + op.payload = payload; + } + return op; + } + + @Override + public void recycleUpdateOp(UpdateOp op) { + if (!mDisableRecycler) { + op.payload = null; + mUpdateOpPool.release(op); + } + } + + void recycleUpdateOpsAndClearList(List<UpdateOp> ops) { + final int count = ops.size(); + for (int i = 0; i < count; i++) { + recycleUpdateOp(ops.get(i)); + } + ops.clear(); + } + + /** + * Contract between AdapterHelper and RecyclerView. + */ + interface Callback { + + RecyclerView.ViewHolder findViewHolder(int position); + + void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); + + void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); + + void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads); + + void onDispatchFirstPass(UpdateOp updateOp); + + void onDispatchSecondPass(UpdateOp updateOp); + + void offsetPositionsForAdd(int positionStart, int itemCount); + + void offsetPositionsForMove(int from, int to); + } +} diff --git a/core/java/com/android/internal/widget/ChildHelper.java b/core/java/com/android/internal/widget/ChildHelper.java new file mode 100644 index 000000000000..e9136d09fb59 --- /dev/null +++ b/core/java/com/android/internal/widget/ChildHelper.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to manage children. + * <p> + * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods + * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods + * like getChildAt, getChildCount etc. These methods ignore hidden children. + * <p> + * When RecyclerView needs direct access to the view group children, it can call unfiltered + * methods like get getUnfilteredChildCount or getUnfilteredChildAt. + */ +class ChildHelper { + + private static final boolean DEBUG = false; + + private static final String TAG = "ChildrenHelper"; + + final Callback mCallback; + + final Bucket mBucket; + + final List<View> mHiddenViews; + + ChildHelper(Callback callback) { + mCallback = callback; + mBucket = new Bucket(); + mHiddenViews = new ArrayList<View>(); + } + + /** + * Marks a child view as hidden + * + * @param child View to hide. + */ + private void hideViewInternal(View child) { + mHiddenViews.add(child); + mCallback.onEnteredHiddenState(child); + } + + /** + * Unmarks a child view as hidden. + * + * @param child View to hide. + */ + private boolean unhideViewInternal(View child) { + if (mHiddenViews.remove(child)) { + mCallback.onLeftHiddenState(child); + return true; + } else { + return false; + } + } + + /** + * Adds a view to the ViewGroup + * + * @param child View to add. + * @param hidden If set to true, this item will be invisible from regular methods. + */ + void addView(View child, boolean hidden) { + addView(child, -1, hidden); + } + + /** + * Add a view to the ViewGroup at an index + * + * @param child View to add. + * @param index Index of the child from the regular perspective (excluding hidden views). + * ChildHelper offsets this index to actual ViewGroup index. + * @param hidden If set to true, this item will be invisible from regular methods. + */ + void addView(View child, int index, boolean hidden) { + final int offset; + if (index < 0) { + offset = mCallback.getChildCount(); + } else { + offset = getOffset(index); + } + mBucket.insert(offset, hidden); + if (hidden) { + hideViewInternal(child); + } + mCallback.addView(child, offset); + if (DEBUG) { + Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this); + } + } + + private int getOffset(int index) { + if (index < 0) { + return -1; //anything below 0 won't work as diff will be undefined. + } + final int limit = mCallback.getChildCount(); + int offset = index; + while (offset < limit) { + final int removedBefore = mBucket.countOnesBefore(offset); + final int diff = index - (offset - removedBefore); + if (diff == 0) { + while (mBucket.get(offset)) { // ensure this offset is not hidden + offset++; + } + return offset; + } else { + offset += diff; + } + } + return -1; + } + + /** + * Removes the provided View from underlying RecyclerView. + * + * @param view The view to remove. + */ + void removeView(View view) { + int index = mCallback.indexOfChild(view); + if (index < 0) { + return; + } + if (mBucket.remove(index)) { + unhideViewInternal(view); + } + mCallback.removeViewAt(index); + if (DEBUG) { + Log.d(TAG, "remove View off:" + index + "," + this); + } + } + + /** + * Removes the view at the provided index from RecyclerView. + * + * @param index Index of the child from the regular perspective (excluding hidden views). + * ChildHelper offsets this index to actual ViewGroup index. + */ + void removeViewAt(int index) { + final int offset = getOffset(index); + final View view = mCallback.getChildAt(offset); + if (view == null) { + return; + } + if (mBucket.remove(offset)) { + unhideViewInternal(view); + } + mCallback.removeViewAt(offset); + if (DEBUG) { + Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this); + } + } + + /** + * Returns the child at provided index. + * + * @param index Index of the child to return in regular perspective. + */ + View getChildAt(int index) { + final int offset = getOffset(index); + return mCallback.getChildAt(offset); + } + + /** + * Removes all views from the ViewGroup including the hidden ones. + */ + void removeAllViewsUnfiltered() { + mBucket.reset(); + for (int i = mHiddenViews.size() - 1; i >= 0; i--) { + mCallback.onLeftHiddenState(mHiddenViews.get(i)); + mHiddenViews.remove(i); + } + mCallback.removeAllViews(); + if (DEBUG) { + Log.d(TAG, "removeAllViewsUnfiltered"); + } + } + + /** + * This can be used to find a disappearing view by position. + * + * @param position The adapter position of the item. + * @return A hidden view with a valid ViewHolder that matches the position. + */ + View findHiddenNonRemovedView(int position) { + final int count = mHiddenViews.size(); + for (int i = 0; i < count; i++) { + final View view = mHiddenViews.get(i); + RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); + if (holder.getLayoutPosition() == position + && !holder.isInvalid() + && !holder.isRemoved()) { + return view; + } + } + return null; + } + + /** + * Attaches the provided view to the underlying ViewGroup. + * + * @param child Child to attach. + * @param index Index of the child to attach in regular perspective. + * @param layoutParams LayoutParams for the child. + * @param hidden If set to true, this item will be invisible to the regular methods. + */ + void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, + boolean hidden) { + final int offset; + if (index < 0) { + offset = mCallback.getChildCount(); + } else { + offset = getOffset(index); + } + mBucket.insert(offset, hidden); + if (hidden) { + hideViewInternal(child); + } + mCallback.attachViewToParent(child, offset, layoutParams); + if (DEBUG) { + Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," + + "h:" + hidden + ", " + this); + } + } + + /** + * Returns the number of children that are not hidden. + * + * @return Number of children that are not hidden. + * @see #getChildAt(int) + */ + int getChildCount() { + return mCallback.getChildCount() - mHiddenViews.size(); + } + + /** + * Returns the total number of children. + * + * @return The total number of children including the hidden views. + * @see #getUnfilteredChildAt(int) + */ + int getUnfilteredChildCount() { + return mCallback.getChildCount(); + } + + /** + * Returns a child by ViewGroup offset. ChildHelper won't offset this index. + * + * @param index ViewGroup index of the child to return. + * @return The view in the provided index. + */ + View getUnfilteredChildAt(int index) { + return mCallback.getChildAt(index); + } + + /** + * Detaches the view at the provided index. + * + * @param index Index of the child to return in regular perspective. + */ + void detachViewFromParent(int index) { + final int offset = getOffset(index); + mBucket.remove(offset); + mCallback.detachViewFromParent(offset); + if (DEBUG) { + Log.d(TAG, "detach view from parent " + index + ", off:" + offset); + } + } + + /** + * Returns the index of the child in regular perspective. + * + * @param child The child whose index will be returned. + * @return The regular perspective index of the child or -1 if it does not exists. + */ + int indexOfChild(View child) { + final int index = mCallback.indexOfChild(child); + if (index == -1) { + return -1; + } + if (mBucket.get(index)) { + if (DEBUG) { + throw new IllegalArgumentException("cannot get index of a hidden child"); + } else { + return -1; + } + } + // reverse the index + return index - mBucket.countOnesBefore(index); + } + + /** + * Returns whether a View is visible to LayoutManager or not. + * + * @param view The child view to check. Should be a child of the Callback. + * @return True if the View is not visible to LayoutManager + */ + boolean isHidden(View view) { + return mHiddenViews.contains(view); + } + + /** + * Marks a child view as hidden. + * + * @param view The view to hide. + */ + void hide(View view) { + final int offset = mCallback.indexOfChild(view); + if (offset < 0) { + throw new IllegalArgumentException("view is not a child, cannot hide " + view); + } + if (DEBUG && mBucket.get(offset)) { + throw new RuntimeException("trying to hide same view twice, how come ? " + view); + } + mBucket.set(offset); + hideViewInternal(view); + if (DEBUG) { + Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this); + } + } + + /** + * Moves a child view from hidden list to regular list. + * Calling this method should probably be followed by a detach, otherwise, it will suddenly + * show up in LayoutManager's children list. + * + * @param view The hidden View to unhide + */ + void unhide(View view) { + final int offset = mCallback.indexOfChild(view); + if (offset < 0) { + throw new IllegalArgumentException("view is not a child, cannot hide " + view); + } + if (!mBucket.get(offset)) { + throw new RuntimeException("trying to unhide a view that was not hidden" + view); + } + mBucket.clear(offset); + unhideViewInternal(view); + } + + @Override + public String toString() { + return mBucket.toString() + ", hidden list:" + mHiddenViews.size(); + } + + /** + * Removes a view from the ViewGroup if it is hidden. + * + * @param view The view to remove. + * @return True if the View is found and it is hidden. False otherwise. + */ + boolean removeViewIfHidden(View view) { + final int index = mCallback.indexOfChild(view); + if (index == -1) { + if (unhideViewInternal(view) && DEBUG) { + throw new IllegalStateException("view is in hidden list but not in view group"); + } + return true; + } + if (mBucket.get(index)) { + mBucket.remove(index); + if (!unhideViewInternal(view) && DEBUG) { + throw new IllegalStateException( + "removed a hidden view but it is not in hidden views list"); + } + mCallback.removeViewAt(index); + return true; + } + return false; + } + + /** + * Bitset implementation that provides methods to offset indices. + */ + static class Bucket { + + static final int BITS_PER_WORD = Long.SIZE; + + static final long LAST_BIT = 1L << (Long.SIZE - 1); + + long mData = 0; + + Bucket mNext; + + void set(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + mNext.set(index - BITS_PER_WORD); + } else { + mData |= 1L << index; + } + } + + private void ensureNext() { + if (mNext == null) { + mNext = new Bucket(); + } + } + + void clear(int index) { + if (index >= BITS_PER_WORD) { + if (mNext != null) { + mNext.clear(index - BITS_PER_WORD); + } + } else { + mData &= ~(1L << index); + } + + } + + boolean get(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return mNext.get(index - BITS_PER_WORD); + } else { + return (mData & (1L << index)) != 0; + } + } + + void reset() { + mData = 0; + if (mNext != null) { + mNext.reset(); + } + } + + void insert(int index, boolean value) { + if (index >= BITS_PER_WORD) { + ensureNext(); + mNext.insert(index - BITS_PER_WORD, value); + } else { + final boolean lastBit = (mData & LAST_BIT) != 0; + long mask = (1L << index) - 1; + final long before = mData & mask; + final long after = ((mData & ~mask)) << 1; + mData = before | after; + if (value) { + set(index); + } else { + clear(index); + } + if (lastBit || mNext != null) { + ensureNext(); + mNext.insert(0, lastBit); + } + } + } + + boolean remove(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return mNext.remove(index - BITS_PER_WORD); + } else { + long mask = (1L << index); + final boolean value = (mData & mask) != 0; + mData &= ~mask; + mask = mask - 1; + final long before = mData & mask; + // cannot use >> because it adds one. + final long after = Long.rotateRight(mData & ~mask, 1); + mData = before | after; + if (mNext != null) { + if (mNext.get(0)) { + set(BITS_PER_WORD - 1); + } + mNext.remove(0); + } + return value; + } + } + + int countOnesBefore(int index) { + if (mNext == null) { + if (index >= BITS_PER_WORD) { + return Long.bitCount(mData); + } + return Long.bitCount(mData & ((1L << index) - 1)); + } + if (index < BITS_PER_WORD) { + return Long.bitCount(mData & ((1L << index) - 1)); + } else { + return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); + } + } + + @Override + public String toString() { + return mNext == null ? Long.toBinaryString(mData) + : mNext.toString() + "xx" + Long.toBinaryString(mData); + } + } + + interface Callback { + + int getChildCount(); + + void addView(View child, int index); + + int indexOfChild(View view); + + void removeViewAt(int index); + + View getChildAt(int offset); + + void removeAllViews(); + + RecyclerView.ViewHolder getChildViewHolder(View view); + + void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); + + void detachViewFromParent(int offset); + + void onEnteredHiddenState(View child); + + void onLeftHiddenState(View child); + } +} + diff --git a/core/java/com/android/internal/widget/DefaultItemAnimator.java b/core/java/com/android/internal/widget/DefaultItemAnimator.java new file mode 100644 index 000000000000..92345af9d118 --- /dev/null +++ b/core/java/com/android/internal/widget/DefaultItemAnimator.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import com.android.internal.widget.RecyclerView.ViewHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * This implementation of {@link RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. + * + * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) + */ +public class DefaultItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private static TimeInterpolator sDefaultInterpolator; + + private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>(); + private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>(); + private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>(); + private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>(); + + ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>(); + ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>(); + ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>(); + + ArrayList<ViewHolder> mAddAnimations = new ArrayList<>(); + ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>(); + ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>(); + ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList<MoveInfo> moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + view.postOnAnimationDelayed(mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList<ChangeInfo> changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + ViewHolder holder = changes.get(0).oldHolder; + holder.itemView.postOnAnimationDelayed(changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList<ViewHolder> additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + @Override + public void run() { + for (ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + view.postOnAnimationDelayed(adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()).alpha(0).setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + view.setAlpha(1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateAdd(final ViewHolder holder) { + resetAnimation(holder); + holder.itemView.setAlpha(0); + mPendingAdditions.add(holder); + return true; + } + + void animateAddImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mAddAnimations.add(holder); + animation.alpha(1).setDuration(getAddDuration()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + view.setAlpha(1); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += holder.itemView.getTranslationX(); + fromY += holder.itemView.getTranslationY(); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + view.setTranslationX(-deltaX); + } + if (deltaY != 0) { + view.setTranslationY(-deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + view.animate().translationX(0); + } + if (deltaY != 0) { + view.animate().translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromX, fromY, toX, toY); + } + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + oldHolder.itemView.setTranslationX(prevTranslationX); + oldHolder.itemView.setTranslationY(prevTranslationY); + oldHolder.itemView.setAlpha(prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + newHolder.itemView.setTranslationX(-deltaX); + newHolder.itemView.setTranslationY(-deltaY); + newHolder.itemView.setAlpha(0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + void animateChangeImpl(final ChangeInfo changeInfo) { + final ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(Animator animator) { + oldViewAnim.setListener(null); + view.setAlpha(1); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimator newViewAnimation = newView.animate(); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) + .alpha(1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + @Override + public void onAnimationEnd(Animator animator) { + newViewAnimation.setListener(null); + newView.setAlpha(1); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + item.itemView.setAlpha(1); + item.itemView.setTranslationX(0); + item.itemView.setTranslationY(0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + view.animate().cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + view.setAlpha(1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList<ChangeInfo> changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList<MoveInfo> moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList<ViewHolder> additions = mAdditionsList.get(i); + if (additions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + private void resetAnimation(ViewHolder holder) { + if (sDefaultInterpolator == null) { + sDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + holder.itemView.animate().setInterpolator(sDefaultInterpolator); + endAnimation(holder); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() + || !mPendingChanges.isEmpty() + || !mPendingMoves.isEmpty() + || !mPendingRemovals.isEmpty() + || !mMoveAnimations.isEmpty() + || !mRemoveAnimations.isEmpty() + || !mAddAnimations.isEmpty() + || !mChangeAnimations.isEmpty() + || !mMovesList.isEmpty() + || !mAdditionsList.isEmpty() + || !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingAdditions.get(i); + item.itemView.setAlpha(1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList<MoveInfo> moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + ViewHolder item = moveInfo.holder; + View view = item.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList<ViewHolder> additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + ViewHolder item = additions.get(j); + View view = item.itemView; + view.setAlpha(1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList<ChangeInfo> changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List<ViewHolder> viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + viewHolders.get(i).itemView.animate().cancel(); + } + } + + /** + * {@inheritDoc} + * <p> + * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>. + * When this is the case: + * <ul> + * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both + * ViewHolder arguments will be the same instance. + * </li> + * <li> + * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, + * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and + * run a move animation instead. + * </li> + * </ul> + */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List<Object> payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } +} diff --git a/core/java/com/android/internal/widget/GapWorker.java b/core/java/com/android/internal/widget/GapWorker.java new file mode 100644 index 000000000000..5972396b53a3 --- /dev/null +++ b/core/java/com/android/internal/widget/GapWorker.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.Nullable; +import android.os.Trace; +import android.view.View; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +final class GapWorker implements Runnable { + + static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>(); + + ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>(); + long mPostTimeNs; + long mFrameIntervalNs; + + static class Task { + public boolean immediate; + public int viewVelocity; + public int distanceToItem; + public RecyclerView view; + public int position; + + public void clear() { + immediate = false; + viewVelocity = 0; + distanceToItem = 0; + view = null; + position = 0; + } + } + + /** + * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects + * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared + * in between calls. + */ + private ArrayList<Task> mTasks = new ArrayList<>(); + + /** + * Prefetch information associated with a specific RecyclerView. + */ + static class LayoutPrefetchRegistryImpl + implements RecyclerView.LayoutManager.LayoutPrefetchRegistry { + int mPrefetchDx; + int mPrefetchDy; + int[] mPrefetchArray; + + int mCount; + + void setPrefetchVector(int dx, int dy) { + mPrefetchDx = dx; + mPrefetchDy = dy; + } + + void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { + mCount = 0; + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + + final RecyclerView.LayoutManager layout = view.mLayout; + if (view.mAdapter != null + && layout != null + && layout.isItemPrefetchEnabled()) { + if (nested) { + // nested prefetch, only if no adapter updates pending. Note: we don't query + // view.hasPendingAdapterUpdates(), as first layout may not have occurred + if (!view.mAdapterHelper.hasPendingUpdates()) { + layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); + } + } else { + // momentum based prefetch, only if we trust current child/adapter state + if (!view.hasPendingAdapterUpdates()) { + layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, + view.mState, this); + } + } + + if (mCount > layout.mPrefetchMaxCountObserved) { + layout.mPrefetchMaxCountObserved = mCount; + layout.mPrefetchMaxObservedInInitialPrefetch = nested; + view.mRecycler.updateViewCacheSize(); + } + } + } + + @Override + public void addPosition(int layoutPosition, int pixelDistance) { + if (pixelDistance < 0) { + throw new IllegalArgumentException("Pixel distance must be non-negative"); + } + + // allocate or expand array as needed, doubling when needed + final int storagePosition = mCount * 2; + if (mPrefetchArray == null) { + mPrefetchArray = new int[4]; + Arrays.fill(mPrefetchArray, -1); + } else if (storagePosition >= mPrefetchArray.length) { + final int[] oldArray = mPrefetchArray; + mPrefetchArray = new int[storagePosition * 2]; + System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length); + } + + // add position + mPrefetchArray[storagePosition] = layoutPosition; + mPrefetchArray[storagePosition + 1] = pixelDistance; + + mCount++; + } + + boolean lastPrefetchIncludedPosition(int position) { + if (mPrefetchArray != null) { + final int count = mCount * 2; + for (int i = 0; i < count; i += 2) { + if (mPrefetchArray[i] == position) return true; + } + } + return false; + } + + /** + * Called when prefetch indices are no longer valid for cache prioritization. + */ + void clearPrefetchPositions() { + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + } + } + + public void add(RecyclerView recyclerView) { + if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) { + throw new IllegalStateException("RecyclerView already present in worker list!"); + } + mRecyclerViews.add(recyclerView); + } + + public void remove(RecyclerView recyclerView) { + boolean removeSuccess = mRecyclerViews.remove(recyclerView); + if (RecyclerView.DEBUG && !removeSuccess) { + throw new IllegalStateException("RecyclerView removal failed!"); + } + } + + /** + * Schedule a prefetch immediately after the current traversal. + */ + void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) { + if (recyclerView.isAttachedToWindow()) { + if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) { + throw new IllegalStateException("attempting to post unregistered view!"); + } + if (mPostTimeNs == 0) { + mPostTimeNs = recyclerView.getNanoTime(); + recyclerView.post(this); + } + } + + recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy); + } + + static Comparator<Task> sTaskComparator = new Comparator<Task>() { + @Override + public int compare(Task lhs, Task rhs) { + // first, prioritize non-cleared tasks + if ((lhs.view == null) != (rhs.view == null)) { + return lhs.view == null ? 1 : -1; + } + + // then prioritize immediate + if (lhs.immediate != rhs.immediate) { + return lhs.immediate ? -1 : 1; + } + + // then prioritize _highest_ view velocity + int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity; + if (deltaViewVelocity != 0) return deltaViewVelocity; + + // then prioritize _lowest_ distance to item + int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem; + if (deltaDistanceToItem != 0) return deltaDistanceToItem; + + return 0; + } + }; + + private void buildTaskList() { + // Update PrefetchRegistry in each view + final int viewCount = mRecyclerViews.size(); + int totalTaskCount = 0; + for (int i = 0; i < viewCount; i++) { + RecyclerView view = mRecyclerViews.get(i); + view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false); + totalTaskCount += view.mPrefetchRegistry.mCount; + } + + // Populate task list from prefetch data... + mTasks.ensureCapacity(totalTaskCount); + int totalTaskIndex = 0; + for (int i = 0; i < viewCount; i++) { + RecyclerView view = mRecyclerViews.get(i); + LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry; + final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) + + Math.abs(prefetchRegistry.mPrefetchDy); + for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) { + final Task task; + if (totalTaskIndex >= mTasks.size()) { + task = new Task(); + mTasks.add(task); + } else { + task = mTasks.get(totalTaskIndex); + } + final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1]; + + task.immediate = distanceToItem <= viewVelocity; + task.viewVelocity = viewVelocity; + task.distanceToItem = distanceToItem; + task.view = view; + task.position = prefetchRegistry.mPrefetchArray[j]; + + totalTaskIndex++; + } + } + + // ... and priority sort + Collections.sort(mTasks, sTaskComparator); + } + + static boolean isPrefetchPositionAttached(RecyclerView view, int position) { + final int childCount = view.mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + View attachedView = view.mChildHelper.getUnfilteredChildAt(i); + RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView); + // Note: can use mPosition here because adapter doesn't have pending updates + if (holder.mPosition == position && !holder.isInvalid()) { + return true; + } + } + return false; + } + + private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, + int position, long deadlineNs) { + if (isPrefetchPositionAttached(view, position)) { + // don't attempt to prefetch attached views + return null; + } + + RecyclerView.Recycler recycler = view.mRecycler; + RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline( + position, false, deadlineNs); + + if (holder != null) { + if (holder.isBound()) { + // Only give the view a chance to go into the cache if binding succeeded + // Note that we must use public method, since item may need cleanup + recycler.recycleView(holder.itemView); + } else { + // Didn't bind, so we can't cache the view, but it will stay in the pool until + // next prefetch/traversal. If a View fails to bind, it means we didn't have + // enough time prior to the deadline (and won't for other instances of this + // type, during this GapWorker prefetch pass). + recycler.addViewHolderToRecycledViewPool(holder, false); + } + } + return holder; + } + + private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView, + long deadlineNs) { + if (innerView == null) { + return; + } + + if (innerView.mDataSetHasChangedAfterLayout + && innerView.mChildHelper.getUnfilteredChildCount() != 0) { + // RecyclerView has new data, but old attached views. Clear everything, so that + // we can prefetch without partially stale data. + innerView.removeAndRecycleViews(); + } + + // do nested prefetch! + final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry; + innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true); + + if (innerPrefetchRegistry.mCount != 0) { + try { + Trace.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG); + innerView.mState.prepareForNestedPrefetch(innerView.mAdapter); + for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) { + // Note that we ignore immediate flag for inner items because + // we have lower confidence they're needed next frame. + final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i]; + prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs); + } + } finally { + Trace.endSection(); + } + } + } + + private void flushTaskWithDeadline(Task task, long deadlineNs) { + long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs; + RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view, + task.position, taskDeadlineNs); + if (holder != null && holder.mNestedRecyclerView != null) { + prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs); + } + } + + private void flushTasksWithDeadline(long deadlineNs) { + for (int i = 0; i < mTasks.size(); i++) { + final Task task = mTasks.get(i); + if (task.view == null) { + break; // done with populated tasks + } + flushTaskWithDeadline(task, deadlineNs); + task.clear(); + } + } + + void prefetch(long deadlineNs) { + buildTaskList(); + flushTasksWithDeadline(deadlineNs); + } + + @Override + public void run() { + try { + Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG); + + if (mRecyclerViews.isEmpty()) { + // abort - no work to do + return; + } + + // Query last vsync so we can predict next one. Note that drawing time not yet + // valid in animation/input callbacks, so query it here to be safe. + long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos( + mRecyclerViews.get(0).getDrawingTime()); + if (lastFrameVsyncNs == 0) { + // abort - couldn't get last vsync for estimating next + return; + } + + // TODO: consider rebasing deadline if frame was already dropped due to long UI work. + // Next frame will still wait for VSYNC, so we can still use the gap if it exists. + long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs; + + prefetch(nextFrameNs); + + // TODO: consider rescheduling self, if there's more work to do + } finally { + mPostTimeNs = 0; + Trace.endSection(); + } + } +} diff --git a/core/java/com/android/internal/widget/LinearLayoutManager.java b/core/java/com/android/internal/widget/LinearLayoutManager.java new file mode 100644 index 000000000000..d82c746649e2 --- /dev/null +++ b/core/java/com/android/internal/widget/LinearLayoutManager.java @@ -0,0 +1,2398 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import static com.android.internal.widget.RecyclerView.NO_POSITION; + +import android.content.Context; +import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import com.android.internal.widget.RecyclerView.LayoutParams; +import com.android.internal.widget.helper.ItemTouchHelper; + +import java.util.List; + +/** + * A {@link com.android.internal.widget.RecyclerView.LayoutManager} implementation which provides + * similar functionality to {@link android.widget.ListView}. + */ +public class LinearLayoutManager extends RecyclerView.LayoutManager implements + ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { + + private static final String TAG = "LinearLayoutManager"; + + static final boolean DEBUG = false; + + public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; + + public static final int VERTICAL = OrientationHelper.VERTICAL; + + public static final int INVALID_OFFSET = Integer.MIN_VALUE; + + + /** + * While trying to find next view to focus, LayoutManager will not try to scroll more + * than this factor times the total space of the list. If layout is vertical, total space is the + * height minus padding, if layout is horizontal, total space is the width minus padding. + */ + private static final float MAX_SCROLL_FACTOR = 1 / 3f; + + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + int mOrientation; + + /** + * Helper class that keeps temporary layout state. + * It does not keep state after layout is complete but we still keep a reference to re-use + * the same object. + */ + private LayoutState mLayoutState; + + /** + * Many calculations are made depending on orientation. To keep it clean, this interface + * helps {@link LinearLayoutManager} make those decisions. + * Based on {@link #mOrientation}, an implementation is lazily created in + * {@link #ensureLayoutState} method. + */ + OrientationHelper mOrientationHelper; + + /** + * We need to track this so that we can ignore current position when it changes. + */ + private boolean mLastStackFromEnd; + + + /** + * Defines if layout should be calculated from end to start. + * + * @see #mShouldReverseLayout + */ + private boolean mReverseLayout = false; + + /** + * This keeps the final value for how LayoutManager should start laying out views. + * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. + * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. + */ + boolean mShouldReverseLayout = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and + * it supports both orientations. + * see {@link android.widget.AbsListView#setStackFromBottom(boolean)} + */ + private boolean mStackFromEnd = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + int mPendingScrollPosition = NO_POSITION; + + /** + * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is + * called. + */ + int mPendingScrollPositionOffset = INVALID_OFFSET; + + private boolean mRecycleChildrenOnDetach; + + SavedState mPendingSavedState = null; + + /** + * Re-used variable to keep anchor information on re-layout. + * Anchor position and coordinate defines the reference point for LLM while doing a layout. + * */ + final AnchorInfo mAnchorInfo = new AnchorInfo(); + + /** + * Stashed to avoid allocation, currently only used in #fill() + */ + private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult(); + + /** + * Number of items to prefetch when first coming on screen with new data. + */ + private int mInitialItemPrefetchCount = 2; + + /** + * Creates a vertical LinearLayoutManager + * + * @param context Current context, will be used to access resources. + */ + public LinearLayoutManager(Context context) { + this(context, VERTICAL, false); + } + + /** + * @param context Current context, will be used to access resources. + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link + * #VERTICAL}. + * @param reverseLayout When set to true, layouts from end to start. + */ + public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + setOrientation(orientation); + setReverseLayout(reverseLayout); + setAutoMeasureEnabled(true); + } + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to vertical orientation. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + */ + public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setOrientation(properties.orientation); + setReverseLayout(properties.reverseLayout); + setStackFromEnd(properties.stackFromEnd); + setAutoMeasureEnabled(true); + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * <p> + * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to <code>true</code> so that views will be available to other RecyclerViews + * immediately. + * <p> + * Note that, setting this flag will result in a performance drop if RecyclerView + * is restored. + * + * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. + */ + public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { + mRecycleChildrenOnDetach = recycleChildrenOnDetach; + } + + @Override + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + if (mRecycleChildrenOnDetach) { + removeAndRecycleAllViews(recycler); + recycler.clear(); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (getChildCount() > 0) { + event.setFromIndex(findFirstVisibleItemPosition()); + event.setToIndex(findLastVisibleItemPosition()); + } + } + + @Override + public Parcelable onSaveInstanceState() { + if (mPendingSavedState != null) { + return new SavedState(mPendingSavedState); + } + SavedState state = new SavedState(); + if (getChildCount() > 0) { + ensureLayoutState(); + boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; + state.mAnchorLayoutFromEnd = didLayoutFromEnd; + if (didLayoutFromEnd) { + final View refChild = getChildClosestToEnd(); + state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(refChild); + state.mAnchorPosition = getPosition(refChild); + } else { + final View refChild = getChildClosestToStart(); + state.mAnchorPosition = getPosition(refChild); + state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) + - mOrientationHelper.getStartAfterPadding(); + } + } else { + state.invalidateAnchor(); + } + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; + requestLayout(); + if (DEBUG) { + Log.d(TAG, "loaded saved state"); + } + } else if (DEBUG) { + Log.d(TAG, "invalid saved state class"); + } + } + + /** + * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} + */ + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + /** + * @return true if {@link #getOrientation()} is {@link #VERTICAL} + */ + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + /** + * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} + */ + public void setStackFromEnd(boolean stackFromEnd) { + assertNotInLayoutOrScroll(null); + if (mStackFromEnd == stackFromEnd) { + return; + } + mStackFromEnd = stackFromEnd; + requestLayout(); + } + + public boolean getStackFromEnd() { + return mStackFromEnd; + } + + /** + * Returns the current orientation of the layout. + * + * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL} + * @see #setOrientation(int) + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation of the layout. {@link com.android.internal.widget.LinearLayoutManager} + * will do its best to keep scroll position. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation:" + orientation); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + mOrientationHelper = null; + requestLayout(); + } + + /** + * Calculates the view layout order. (e.g. from end to start or start to end) + * RTL layout support is applied automatically. So if layout is RTL and + * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. + */ + private void resolveShouldLayoutReverse() { + // A == B is the same result, but we rather keep it readable + if (mOrientation == VERTICAL || !isLayoutRTL()) { + mShouldReverseLayout = mReverseLayout; + } else { + mShouldReverseLayout = !mReverseLayout; + } + } + + /** + * Returns if views are laid out from the opposite direction of the layout. + * + * @return If layout is reversed or not. + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + + /** + * Used to reverse item traversal and layout order. + * This behaves similar to the layout change for RTL views. When set to true, first item is + * laid out at the end of the UI, second item is laid out before it etc. + * + * For horizontal layouts, it depends on the layout direction. + * When set to true, If {@link com.android.internal.widget.RecyclerView} is LTR, than it will + * layout from RTL, if {@link com.android.internal.widget.RecyclerView}} is RTL, it will layout + * from LTR. + * + * If you are looking for the exact same behavior of + * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use + * {@link #setStackFromEnd(boolean)} + */ + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (reverseLayout == mReverseLayout) { + return; + } + mReverseLayout = reverseLayout; + requestLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public View findViewByPosition(int position) { + final int childCount = getChildCount(); + if (childCount == 0) { + return null; + } + final int firstChild = getPosition(getChildAt(0)); + final int viewPosition = position - firstChild; + if (viewPosition >= 0 && viewPosition < childCount) { + final View child = getChildAt(viewPosition); + if (getPosition(child) == position) { + return child; // in pre-layout, this may not match + } + } + // fallback to traversal. This might be necessary in pre-layout. + return super.findViewByPosition(position); + } + + /** + * <p>Returns the amount of extra space that should be laid out by LayoutManager.</p> + * + * <p>By default, {@link com.android.internal.widget.LinearLayoutManager} lays out 1 extra page + * of items while smooth scrolling and 0 otherwise. You can override this method to implement + * your custom layout pre-cache logic.</p> + * + * <p><strong>Note:</strong>Laying out invisible elements generally comes with significant + * performance cost. It's typically only desirable in places like smooth scrolling to an unknown + * location, where 1) the extra content helps LinearLayoutManager know in advance when its + * target is approaching, so it can decelerate early and smoothly and 2) while motion is + * continuous.</p> + * + * <p>Extending the extra layout space is especially expensive if done while the user may change + * scrolling direction. Changing direction will cause the extra layout space to swap to the + * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large + * enough to handle it.</p> + * + * @return The extra space that should be laid out (in pixels). + */ + protected int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return mOrientationHelper.getTotalSpace(); + } else { + return 0; + } + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { + LinearSmoothScroller linearSmoothScroller = + new LinearSmoothScroller(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } + final int firstChildPos = getPosition(getChildAt(0)); + final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; + if (mOrientation == HORIZONTAL) { + return new PointF(direction, 0); + } else { + return new PointF(0, direction); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + // layout algorithm: + // 1) by checking children and other variables, find an anchor coordinate and an anchor + // item position. + // 2) fill towards start, stacking from bottom + // 3) fill towards end, stacking from top + // 4) scroll to fulfill requirements like stack from bottom. + // create layout state + if (DEBUG) { + Log.d(TAG, "is pre layout:" + state.isPreLayout()); + } + if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) { + if (state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + return; + } + } + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + } + + ensureLayoutState(); + mLayoutState.mRecycle = false; + // resolve layout direction + resolveShouldLayoutReverse(); + + if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION + || mPendingSavedState != null) { + mAnchorInfo.reset(); + mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; + // calculate anchor position and coordinate + updateAnchorInfoForLayout(recycler, state, mAnchorInfo); + mAnchorInfo.mValid = true; + } + if (DEBUG) { + Log.d(TAG, "Anchor info:" + mAnchorInfo); + } + + // LLM may decide to layout items for "extra" pixels to account for scrolling target, + // caching or predictive animations. + int extraForStart; + int extraForEnd; + final int extra = getExtraLayoutSpace(state); + // If the previous scroll delta was less than zero, the extra space should be laid out + // at the start. Otherwise, it should be at the end. + if (mLayoutState.mLastScrollDelta >= 0) { + extraForEnd = extra; + extraForStart = 0; + } else { + extraForStart = extra; + extraForEnd = 0; + } + extraForStart += mOrientationHelper.getStartAfterPadding(); + extraForEnd += mOrientationHelper.getEndPadding(); + if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION + && mPendingScrollPositionOffset != INVALID_OFFSET) { + // if the child is visible and we are going to move it around, we should layout + // extra items in the opposite direction to make sure new items animate nicely + // instead of just fading in + final View existing = findViewByPosition(mPendingScrollPosition); + if (existing != null) { + final int current; + final int upcomingOffset; + if (mShouldReverseLayout) { + current = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(existing); + upcomingOffset = current - mPendingScrollPositionOffset; + } else { + current = mOrientationHelper.getDecoratedStart(existing) + - mOrientationHelper.getStartAfterPadding(); + upcomingOffset = mPendingScrollPositionOffset - current; + } + if (upcomingOffset > 0) { + extraForStart += upcomingOffset; + } else { + extraForEnd -= upcomingOffset; + } + } + } + int startOffset; + int endOffset; + final int firstLayoutDirection; + if (mAnchorInfo.mLayoutFromEnd) { + firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + } else { + firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + } + + onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); + detachAndScrapAttachedViews(recycler); + mLayoutState.mInfinite = resolveIsInfinite(); + mLayoutState.mIsPreLayout = state.isPreLayout(); + if (mAnchorInfo.mLayoutFromEnd) { + // fill towards start + updateLayoutStateToFillStart(mAnchorInfo); + mLayoutState.mExtra = extraForStart; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + final int firstElement = mLayoutState.mCurrentPosition; + if (mLayoutState.mAvailable > 0) { + extraForEnd += mLayoutState.mAvailable; + } + // fill towards end + updateLayoutStateToFillEnd(mAnchorInfo); + mLayoutState.mExtra = extraForEnd; + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + + if (mLayoutState.mAvailable > 0) { + // end could not consume all. add more items towards start + extraForStart = mLayoutState.mAvailable; + updateLayoutStateToFillStart(firstElement, startOffset); + mLayoutState.mExtra = extraForStart; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + } + } else { + // fill towards end + updateLayoutStateToFillEnd(mAnchorInfo); + mLayoutState.mExtra = extraForEnd; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + final int lastElement = mLayoutState.mCurrentPosition; + if (mLayoutState.mAvailable > 0) { + extraForStart += mLayoutState.mAvailable; + } + // fill towards start + updateLayoutStateToFillStart(mAnchorInfo); + mLayoutState.mExtra = extraForStart; + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + + if (mLayoutState.mAvailable > 0) { + extraForEnd = mLayoutState.mAvailable; + // start could not consume all it should. add more items towards end + updateLayoutStateToFillEnd(lastElement, endOffset); + mLayoutState.mExtra = extraForEnd; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + } + } + + // changes may cause gaps on the UI, try to fix them. + // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have + // changed + if (getChildCount() > 0) { + // because layout from end may be changed by scroll to position + // we re-calculate it. + // find which side we should check for gaps. + if (mShouldReverseLayout ^ mStackFromEnd) { + int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); + startOffset += fixOffset; + endOffset += fixOffset; + fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); + startOffset += fixOffset; + endOffset += fixOffset; + } else { + int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); + startOffset += fixOffset; + endOffset += fixOffset; + fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); + startOffset += fixOffset; + endOffset += fixOffset; + } + } + layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); + if (!state.isPreLayout()) { + mOrientationHelper.onLayoutComplete(); + } else { + mAnchorInfo.reset(); + } + mLastStackFromEnd = mStackFromEnd; + if (DEBUG) { + validateChildOrder(); + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; // we don't need this anymore + mPendingScrollPosition = NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + mAnchorInfo.reset(); + } + + /** + * Method called when Anchor position is decided. Extending class can setup accordingly or + * even update anchor info if necessary. + * @param recycler The recycler for the layout + * @param state The layout state + * @param anchorInfo The mutable POJO that keeps the position and offset. + * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter + * indices. + */ + void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, + AnchorInfo anchorInfo, int firstLayoutItemDirection) { + } + + /** + * If necessary, layouts new items for predictive animations + */ + private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, + RecyclerView.State state, int startOffset, int endOffset) { + // If there are scrap children that we did not layout, we need to find where they did go + // and layout them accordingly so that animations can work as expected. + // This case may happen if new views are added or an existing view expands and pushes + // another view out of bounds. + if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout() + || !supportsPredictiveItemAnimations()) { + return; + } + // to make the logic simpler, we calculate the size of children and call fill. + int scrapExtraStart = 0, scrapExtraEnd = 0; + final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); + final int scrapSize = scrapList.size(); + final int firstChildPos = getPosition(getChildAt(0)); + for (int i = 0; i < scrapSize; i++) { + RecyclerView.ViewHolder scrap = scrapList.get(i); + if (scrap.isRemoved()) { + continue; + } + final int position = scrap.getLayoutPosition(); + final int direction = position < firstChildPos != mShouldReverseLayout + ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; + if (direction == LayoutState.LAYOUT_START) { + scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); + } else { + scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); + } + } + + if (DEBUG) { + Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart + + " towards start and " + scrapExtraEnd + " towards end"); + } + mLayoutState.mScrapList = scrapList; + if (scrapExtraStart > 0) { + View anchor = getChildClosestToStart(); + updateLayoutStateToFillStart(getPosition(anchor), startOffset); + mLayoutState.mExtra = scrapExtraStart; + mLayoutState.mAvailable = 0; + mLayoutState.assignPositionFromScrapList(); + fill(recycler, mLayoutState, state, false); + } + + if (scrapExtraEnd > 0) { + View anchor = getChildClosestToEnd(); + updateLayoutStateToFillEnd(getPosition(anchor), endOffset); + mLayoutState.mExtra = scrapExtraEnd; + mLayoutState.mAvailable = 0; + mLayoutState.assignPositionFromScrapList(); + fill(recycler, mLayoutState, state, false); + } + mLayoutState.mScrapList = null; + } + + private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, + AnchorInfo anchorInfo) { + if (updateAnchorFromPendingData(state, anchorInfo)) { + if (DEBUG) { + Log.d(TAG, "updated anchor info from pending information"); + } + return; + } + + if (updateAnchorFromChildren(recycler, state, anchorInfo)) { + if (DEBUG) { + Log.d(TAG, "updated anchor info from existing children"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "deciding anchor info for fresh state"); + } + anchorInfo.assignCoordinateFromPadding(); + anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; + } + + /** + * Finds an anchor child from existing Views. Most of the time, this is the view closest to + * start or end that has a valid position (e.g. not removed). + * <p> + * If a child has focus, it is given priority. + */ + private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, + RecyclerView.State state, AnchorInfo anchorInfo) { + if (getChildCount() == 0) { + return false; + } + final View focused = getFocusedChild(); + if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) { + anchorInfo.assignFromViewAndKeepVisibleRect(focused); + return true; + } + if (mLastStackFromEnd != mStackFromEnd) { + return false; + } + View referenceChild = anchorInfo.mLayoutFromEnd + ? findReferenceChildClosestToEnd(recycler, state) + : findReferenceChildClosestToStart(recycler, state); + if (referenceChild != null) { + anchorInfo.assignFromView(referenceChild); + // If all visible views are removed in 1 pass, reference child might be out of bounds. + // If that is the case, offset it back to 0 so that we use these pre-layout children. + if (!state.isPreLayout() && supportsPredictiveItemAnimations()) { + // validate this child is at least partially visible. if not, offset it to start + final boolean notVisible = + mOrientationHelper.getDecoratedStart(referenceChild) >= mOrientationHelper + .getEndAfterPadding() + || mOrientationHelper.getDecoratedEnd(referenceChild) + < mOrientationHelper.getStartAfterPadding(); + if (notVisible) { + anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd + ? mOrientationHelper.getEndAfterPadding() + : mOrientationHelper.getStartAfterPadding(); + } + } + return true; + } + return false; + } + + /** + * If there is a pending scroll position or saved states, updates the anchor info from that + * data and returns true + */ + private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { + if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) { + return false; + } + // validate scroll position + if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { + mPendingScrollPosition = NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + if (DEBUG) { + Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); + } + return false; + } + + // if child is visible, try to make it a reference child and ensure it is fully visible. + // if child is not visible, align it depending on its virtual position. + anchorInfo.mPosition = mPendingScrollPosition; + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + // Anchor offset depends on how that child was laid out. Here, we update it + // according to our current view bounds + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + if (anchorInfo.mLayoutFromEnd) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() + - mPendingSavedState.mAnchorOffset; + } else { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + + mPendingSavedState.mAnchorOffset; + } + return true; + } + + if (mPendingScrollPositionOffset == INVALID_OFFSET) { + View child = findViewByPosition(mPendingScrollPosition); + if (child != null) { + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); + if (childSize > mOrientationHelper.getTotalSpace()) { + // item does not fit. fix depending on layout direction + anchorInfo.assignCoordinateFromPadding(); + return true; + } + final int startGap = mOrientationHelper.getDecoratedStart(child) + - mOrientationHelper.getStartAfterPadding(); + if (startGap < 0) { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); + anchorInfo.mLayoutFromEnd = false; + return true; + } + final int endGap = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(child); + if (endGap < 0) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); + anchorInfo.mLayoutFromEnd = true; + return true; + } + anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd + ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper + .getTotalSpaceChange()) + : mOrientationHelper.getDecoratedStart(child); + } else { // item is not visible. + if (getChildCount() > 0) { + // get position of any child, does not matter + int pos = getPosition(getChildAt(0)); + anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos + == mShouldReverseLayout; + } + anchorInfo.assignCoordinateFromPadding(); + } + return true; + } + // override layout from end values for consistency + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + // if this changes, we should update prepareForDrop as well + if (mShouldReverseLayout) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() + - mPendingScrollPositionOffset; + } else { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + + mPendingScrollPositionOffset; + } + return true; + } + + /** + * @return The final offset amount for children + */ + private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, + RecyclerView.State state, boolean canOffsetChildren) { + int gap = mOrientationHelper.getEndAfterPadding() - endOffset; + int fixOffset = 0; + if (gap > 0) { + fixOffset = -scrollBy(-gap, recycler, state); + } else { + return 0; // nothing to fix + } + // move offset according to scroll amount + endOffset += fixOffset; + if (canOffsetChildren) { + // re-calculate gap, see if we could fix it + gap = mOrientationHelper.getEndAfterPadding() - endOffset; + if (gap > 0) { + mOrientationHelper.offsetChildren(gap); + return gap + fixOffset; + } + } + return fixOffset; + } + + /** + * @return The final offset amount for children + */ + private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, + RecyclerView.State state, boolean canOffsetChildren) { + int gap = startOffset - mOrientationHelper.getStartAfterPadding(); + int fixOffset = 0; + if (gap > 0) { + // check if we should fix this gap. + fixOffset = -scrollBy(gap, recycler, state); + } else { + return 0; // nothing to fix + } + startOffset += fixOffset; + if (canOffsetChildren) { + // re-calculate gap, see if we could fix it + gap = startOffset - mOrientationHelper.getStartAfterPadding(); + if (gap > 0) { + mOrientationHelper.offsetChildren(-gap); + return fixOffset - gap; + } + } + return fixOffset; + } + + private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) { + updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); + } + + private void updateLayoutStateToFillEnd(int itemPosition, int offset) { + mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : + LayoutState.ITEM_DIRECTION_TAIL; + mLayoutState.mCurrentPosition = itemPosition; + mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END; + mLayoutState.mOffset = offset; + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + } + + private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) { + updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); + } + + private void updateLayoutStateToFillStart(int itemPosition, int offset) { + mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); + mLayoutState.mCurrentPosition = itemPosition; + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : + LayoutState.ITEM_DIRECTION_HEAD; + mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START; + mLayoutState.mOffset = offset; + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + + } + + protected boolean isLayoutRTL() { + return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + void ensureLayoutState() { + if (mLayoutState == null) { + mLayoutState = createLayoutState(); + } + if (mOrientationHelper == null) { + mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); + } + } + + /** + * Test overrides this to plug some tracking and verification. + * + * @return A new LayoutState + */ + LayoutState createLayoutState() { + return new LayoutState(); + } + + /** + * <p>Scroll the RecyclerView to make the position visible.</p> + * + * <p>RecyclerView will scroll the minimum amount that is necessary to make the + * target position visible. If you are looking for a similar behavior to + * {@link android.widget.ListView#setSelection(int)} or + * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use + * {@link #scrollToPositionWithOffset(int, int)}.</p> + * + * <p>Note that scroll position change will not be reflected until the next layout call.</p> + * + * @param position Scroll to this adapter position + * @see #scrollToPositionWithOffset(int, int) + */ + @Override + public void scrollToPosition(int position) { + mPendingScrollPosition = position; + mPendingScrollPositionOffset = INVALID_OFFSET; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + /** + * Scroll to the specified adapter position with the given offset from resolved layout + * start. Resolved layout start depends on {@link #getReverseLayout()}, + * {@link View#getLayoutDirection()} and {@link #getStackFromEnd()}. + * <p> + * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling + * <code>scrollToPositionWithOffset(10, 20)</code> will layout such that + * <code>item[10]</code>'s bottom is 20 pixels above the RecyclerView's bottom. + * <p> + * Note that scroll position change will not be reflected until the next layout call. + * <p> + * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. + * + * @param position Index (starting at 0) of the reference item. + * @param offset The distance (in pixels) between the start edge of the item view and + * start edge of the RecyclerView. + * @see #setReverseLayout(boolean) + * @see #scrollToPosition(int) + */ + public void scrollToPositionWithOffset(int position, int offset) { + mPendingScrollPosition = position; + mPendingScrollPositionOffset = offset; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + + /** + * {@inheritDoc} + */ + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + /** + * {@inheritDoc} + */ + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + private int computeScrollOffset(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled, mShouldReverseLayout); + } + + private int computeScrollExtent(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled); + } + + private int computeScrollRange(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollRange(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled); + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed + * based on the number of visible pixels in the visible items. This however assumes that all + * list items have similar or equal widths or heights (depending on list orientation). + * If you use a list in which items have different dimensions, the scrollbar will change + * appearance as the user scrolls through the list. To avoid this issue, you need to disable + * this property. + * + * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based + * solely on the number of items in the adapter and the position of the visible items inside + * the adapter. This provides a stable scrollbar as the user navigates through a list of items + * with varying widths / heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * + * @see #setSmoothScrollbarEnabled(boolean) + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + /** + * Returns the current state of the smooth scrollbar feature. It is enabled by default. + * + * @return True if smooth scrollbar is enabled, false otherwise. + * + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + private void updateLayoutState(int layoutDirection, int requiredSpace, + boolean canUseExistingSpace, RecyclerView.State state) { + // If parent provides a hint, don't measure unlimited. + mLayoutState.mInfinite = resolveIsInfinite(); + mLayoutState.mExtra = getExtraLayoutSpace(state); + mLayoutState.mLayoutDirection = layoutDirection; + int scrollingOffset; + if (layoutDirection == LayoutState.LAYOUT_END) { + mLayoutState.mExtra += mOrientationHelper.getEndPadding(); + // get the first child in the direction we are going + final View child = getChildClosestToEnd(); + // the direction in which we are traversing children + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; + mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); + // calculate how much we can scroll without adding new children (independent of layout) + scrollingOffset = mOrientationHelper.getDecoratedEnd(child) + - mOrientationHelper.getEndAfterPadding(); + + } else { + final View child = getChildClosestToStart(); + mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding(); + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; + mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); + scrollingOffset = -mOrientationHelper.getDecoratedStart(child) + + mOrientationHelper.getStartAfterPadding(); + } + mLayoutState.mAvailable = requiredSpace; + if (canUseExistingSpace) { + mLayoutState.mAvailable -= scrollingOffset; + } + mLayoutState.mScrollingOffset = scrollingOffset; + } + + boolean resolveIsInfinite() { + return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED + && mOrientationHelper.getEnd() == 0; + } + + void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final int pos = layoutState.mCurrentPosition; + if (pos >= 0 && pos < state.getItemCount()) { + layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset); + } + } + + @Override + public void collectInitialPrefetchPositions(int adapterItemCount, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final boolean fromEnd; + final int anchorPos; + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + // use restored state, since it hasn't been resolved yet + fromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + anchorPos = mPendingSavedState.mAnchorPosition; + } else { + resolveShouldLayoutReverse(); + fromEnd = mShouldReverseLayout; + if (mPendingScrollPosition == NO_POSITION) { + anchorPos = fromEnd ? adapterItemCount - 1 : 0; + } else { + anchorPos = mPendingScrollPosition; + } + } + + final int direction = fromEnd + ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + int targetPos = anchorPos; + for (int i = 0; i < mInitialItemPrefetchCount; i++) { + if (targetPos >= 0 && targetPos < adapterItemCount) { + layoutPrefetchRegistry.addPosition(targetPos, 0); + } else { + break; // no more to prefetch + } + targetPos += direction; + } + } + + /** + * Sets the number of items to prefetch in + * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines + * how many inner items should be prefetched when this LayoutManager's RecyclerView + * is nested inside another RecyclerView. + * + * <p>Set this value to the number of items this inner LayoutManager will display when it is + * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items + * so they are ready, avoiding jank as the inner RecyclerView is scrolled into the viewport.</p> + * + * <p>For example, take a vertically scrolling RecyclerView with horizontally scrolling inner + * RecyclerViews. The rows always have 4 items visible in them (or 5 if not aligned). Passing + * <code>4</code> to this method for each inner RecyclerView's LinearLayoutManager will enable + * RecyclerView's prefetching feature to do create/bind work for 4 views within a row early, + * before it is scrolled on screen, instead of just the default 2.</p> + * + * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView + * nested in another RecyclerView.</p> + * + * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of + * views that will be visible in this view can incur unnecessary bind work, and an increase to + * the number of Views created and in active use.</p> + * + * @param itemCount Number of items to prefetch + * + * @see #isItemPrefetchEnabled() + * @see #getInitialItemPrefetchCount() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public void setInitialPrefetchItemCount(int itemCount) { + mInitialItemPrefetchCount = itemCount; + } + + /** + * Gets the number of items to prefetch in + * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines + * how many inner items should be prefetched when this LayoutManager's RecyclerView + * is nested inside another RecyclerView. + * + * @see #isItemPrefetchEnabled() + * @see #setInitialPrefetchItemCount(int) + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + * + * @return number of items to prefetch. + */ + public int getInitialItemPrefetchCount() { + return mInitialItemPrefetchCount; + } + + @Override + public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + int delta = (mOrientation == HORIZONTAL) ? dx : dy; + if (getChildCount() == 0 || delta == 0) { + // can't support this scroll, so don't bother prefetching + return; + } + + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDy = Math.abs(delta); + updateLayoutState(layoutDirection, absDy, true, state); + collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry); + } + + int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dy == 0) { + return 0; + } + mLayoutState.mRecycle = true; + ensureLayoutState(); + final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDy = Math.abs(dy); + updateLayoutState(layoutDirection, absDy, true, state); + final int consumed = mLayoutState.mScrollingOffset + + fill(recycler, mLayoutState, state, false); + if (consumed < 0) { + if (DEBUG) { + Log.d(TAG, "Don't have any more elements to scroll"); + } + return 0; + } + final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; + mOrientationHelper.offsetChildren(-scrolled); + if (DEBUG) { + Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); + } + mLayoutState.mLastScrollDelta = scrolled; + return scrolled; + } + + @Override + public void assertNotInLayoutOrScroll(String message) { + if (mPendingSavedState == null) { + super.assertNotInLayoutOrScroll(message); + } + } + + /** + * Recycles children between given indices. + * + * @param startIndex inclusive + * @param endIndex exclusive + */ + private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { + if (startIndex == endIndex) { + return; + } + if (DEBUG) { + Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); + } + if (endIndex > startIndex) { + for (int i = endIndex - 1; i >= startIndex; i--) { + removeAndRecycleViewAt(i, recycler); + } + } else { + for (int i = startIndex; i > endIndex; i--) { + removeAndRecycleViewAt(i, recycler); + } + } + } + + /** + * Recycles views that went out of bounds after scrolling towards the end of the layout. + * <p> + * Checks both layout position and visible position to guarantee that the view is not visible. + * + * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView} + * @param dt This can be used to add additional padding to the visible area. This is used + * to detect children that will go out of bounds after scrolling, without + * actually moving them. + */ + private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { + if (dt < 0) { + if (DEBUG) { + Log.d(TAG, "Called recycle from start with a negative value. This might happen" + + " during layout changes but may be sign of a bug"); + } + return; + } + // ignore padding, ViewGroup may not clip children. + final int limit = dt; + final int childCount = getChildCount(); + if (mShouldReverseLayout) { + for (int i = childCount - 1; i >= 0; i--) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedEnd(child) > limit + || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { + // stop here + recycleChildren(recycler, childCount - 1, i); + return; + } + } + } else { + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedEnd(child) > limit + || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { + // stop here + recycleChildren(recycler, 0, i); + return; + } + } + } + } + + + /** + * Recycles views that went out of bounds after scrolling towards the start of the layout. + * <p> + * Checks both layout position and visible position to guarantee that the view is not visible. + * + * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView} + * @param dt This can be used to add additional padding to the visible area. This is used + * to detect children that will go out of bounds after scrolling, without + * actually moving them. + */ + private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) { + final int childCount = getChildCount(); + if (dt < 0) { + if (DEBUG) { + Log.d(TAG, "Called recycle from end with a negative value. This might happen" + + " during layout changes but may be sign of a bug"); + } + return; + } + final int limit = mOrientationHelper.getEnd() - dt; + if (mShouldReverseLayout) { + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedStart(child) < limit + || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { + // stop here + recycleChildren(recycler, 0, i); + return; + } + } + } else { + for (int i = childCount - 1; i >= 0; i--) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedStart(child) < limit + || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { + // stop here + recycleChildren(recycler, childCount - 1, i); + return; + } + } + } + } + + /** + * Helper method to call appropriate recycle method depending on current layout direction + * + * @param recycler Current recycler that is attached to RecyclerView + * @param layoutState Current layout state. Right now, this object does not change but + * we may consider moving it out of this view so passing around as a + * parameter for now, rather than accessing {@link #mLayoutState} + * @see #recycleViewsFromStart(com.android.internal.widget.RecyclerView.Recycler, int) + * @see #recycleViewsFromEnd(com.android.internal.widget.RecyclerView.Recycler, int) + * @see com.android.internal.widget.LinearLayoutManager.LayoutState#mLayoutDirection + */ + private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (!layoutState.mRecycle || layoutState.mInfinite) { + return; + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); + } else { + recycleViewsFromStart(recycler, layoutState.mScrollingOffset); + } + } + + /** + * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly + * independent from the rest of the {@link com.android.internal.widget.LinearLayoutManager} + * and with little change, can be made publicly available as a helper class. + * + * @param recycler Current recycler that is attached to RecyclerView + * @param layoutState Configuration on how we should fill out the available space. + * @param state Context passed by the RecyclerView to control scroll steps. + * @param stopOnFocusable If true, filling stops in the first focusable new child + * @return Number of pixels that it added. Useful for scroll functions. + */ + int fill(RecyclerView.Recycler recycler, LayoutState layoutState, + RecyclerView.State state, boolean stopOnFocusable) { + // max offset we should set is mFastScroll + available + final int start = layoutState.mAvailable; + if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { + // TODO ugly bug fix. should not happen + if (layoutState.mAvailable < 0) { + layoutState.mScrollingOffset += layoutState.mAvailable; + } + recycleByLayoutState(recycler, layoutState); + } + int remainingSpace = layoutState.mAvailable + layoutState.mExtra; + LayoutChunkResult layoutChunkResult = mLayoutChunkResult; + while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { + layoutChunkResult.resetInternal(); + layoutChunk(recycler, state, layoutState, layoutChunkResult); + if (layoutChunkResult.mFinished) { + break; + } + layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; + /** + * Consume the available space if: + * * layoutChunk did not request to be ignored + * * OR we are laying out scrap children + * * OR we are not doing pre-layout + */ + if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null + || !state.isPreLayout()) { + layoutState.mAvailable -= layoutChunkResult.mConsumed; + // we keep a separate remaining space because mAvailable is important for recycling + remainingSpace -= layoutChunkResult.mConsumed; + } + + if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { + layoutState.mScrollingOffset += layoutChunkResult.mConsumed; + if (layoutState.mAvailable < 0) { + layoutState.mScrollingOffset += layoutState.mAvailable; + } + recycleByLayoutState(recycler, layoutState); + } + if (stopOnFocusable && layoutChunkResult.mFocusable) { + break; + } + } + if (DEBUG) { + validateChildOrder(); + } + return start - layoutState.mAvailable; + } + + void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, + LayoutState layoutState, LayoutChunkResult result) { + View view = layoutState.next(recycler); + if (view == null) { + if (DEBUG && layoutState.mScrapList == null) { + throw new RuntimeException("received null view when unexpected"); + } + // if we are laying out views in scrap, this may return null which means there is + // no more items to layout. + result.mFinished = true; + return; + } + LayoutParams params = (LayoutParams) view.getLayoutParams(); + if (layoutState.mScrapList == null) { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addView(view); + } else { + addView(view, 0); + } + } else { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addDisappearingView(view); + } else { + addDisappearingView(view, 0); + } + } + measureChildWithMargins(view, 0, 0); + result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); + int left, top, right, bottom; + if (mOrientation == VERTICAL) { + if (isLayoutRTL()) { + right = getWidth() - getPaddingRight(); + left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); + } else { + left = getPaddingLeft(); + right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + bottom = layoutState.mOffset; + top = layoutState.mOffset - result.mConsumed; + } else { + top = layoutState.mOffset; + bottom = layoutState.mOffset + result.mConsumed; + } + } else { + top = getPaddingTop(); + bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); + + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + right = layoutState.mOffset; + left = layoutState.mOffset - result.mConsumed; + } else { + left = layoutState.mOffset; + right = layoutState.mOffset + result.mConsumed; + } + } + // We calculate everything with View's bounding box (which includes decor and margins) + // To calculate correct layout position, we subtract margins. + layoutDecoratedWithMargins(view, left, top, right, bottom); + if (DEBUG) { + Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); + } + // Consume the available space if the view is not removed OR changed + if (params.isItemRemoved() || params.isItemChanged()) { + result.mIgnoreConsumed = true; + } + result.mFocusable = view.isFocusable(); + } + + @Override + boolean shouldMeasureTwice() { + return getHeightMode() != View.MeasureSpec.EXACTLY + && getWidthMode() != View.MeasureSpec.EXACTLY + && hasFlexibleChildInBothOrientations(); + } + + /** + * Converts a focusDirection to orientation. + * + * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction + * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. + */ + int convertFocusDirectionToLayoutDirection(int focusDirection) { + switch (focusDirection) { + case View.FOCUS_BACKWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_START; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_END; + } else { + return LayoutState.LAYOUT_START; + } + case View.FOCUS_FORWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_END; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_START; + } else { + return LayoutState.LAYOUT_END; + } + case View.FOCUS_UP: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_DOWN: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_LEFT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_RIGHT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + default: + if (DEBUG) { + Log.d(TAG, "Unknown focus request:" + focusDirection); + } + return LayoutState.INVALID_LAYOUT; + } + + } + + /** + * Convenience method to find the child closes to start. Caller should check it has enough + * children. + * + * @return The child closes to start of the layout from user's perspective. + */ + private View getChildClosestToStart() { + return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0); + } + + /** + * Convenience method to find the child closes to end. Caller should check it has enough + * children. + * + * @return The child closes to end of the layout from user's perspective. + */ + private View getChildClosestToEnd() { + return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1); + } + + /** + * Convenience method to find the visible child closes to start. Caller should check if it has + * enough children. + * + * @param completelyVisible Whether child should be completely visible or not + * @return The first visible child closest to start of the layout from user's perspective. + */ + private View findFirstVisibleChildClosestToStart(boolean completelyVisible, + boolean acceptPartiallyVisible) { + if (mShouldReverseLayout) { + return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, + acceptPartiallyVisible); + } else { + return findOneVisibleChild(0, getChildCount(), completelyVisible, + acceptPartiallyVisible); + } + } + + /** + * Convenience method to find the visible child closes to end. Caller should check if it has + * enough children. + * + * @param completelyVisible Whether child should be completely visible or not + * @return The first visible child closest to end of the layout from user's perspective. + */ + private View findFirstVisibleChildClosestToEnd(boolean completelyVisible, + boolean acceptPartiallyVisible) { + if (mShouldReverseLayout) { + return findOneVisibleChild(0, getChildCount(), completelyVisible, + acceptPartiallyVisible); + } else { + return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, + acceptPartiallyVisible); + } + } + + + /** + * Among the children that are suitable to be considered as an anchor child, returns the one + * closest to the end of the layout. + * <p> + * Due to ambiguous adapter updates or children being removed, some children's positions may be + * invalid. This method is a best effort to find a position within adapter bounds if possible. + * <p> + * It also prioritizes children that are within the visible bounds. + * @return A View that can be used an an anchor View. + */ + private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) : + findLastReferenceChild(recycler, state); + } + + /** + * Among the children that are suitable to be considered as an anchor child, returns the one + * closest to the start of the layout. + * <p> + * Due to ambiguous adapter updates or children being removed, some children's positions may be + * invalid. This method is a best effort to find a position within adapter bounds if possible. + * <p> + * It also prioritizes children that are within the visible bounds. + * + * @return A View that can be used an an anchor View. + */ + private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return mShouldReverseLayout ? findLastReferenceChild(recycler, state) : + findFirstReferenceChild(recycler, state); + } + + private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { + return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount()); + } + + private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { + return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount()); + } + + // overridden by GridLayoutManager + View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, + int start, int end, int itemCount) { + ensureLayoutState(); + View invalidMatch = null; + View outOfBoundsMatch = null; + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); + final int diff = end > start ? 1 : -1; + for (int i = start; i != end; i += diff) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + if (((LayoutParams) view.getLayoutParams()).isItemRemoved()) { + if (invalidMatch == null) { + invalidMatch = view; // removed item, least preferred + } + } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd + || mOrientationHelper.getDecoratedEnd(view) < boundsStart) { + if (outOfBoundsMatch == null) { + outOfBoundsMatch = view; // item is not visible, less preferred + } + } else { + return view; + } + } + } + return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; + } + + /** + * Returns the adapter position of the first visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * <p> + * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + * <p> + * If RecyclerView has item decorators, they will be considered in calculations as well. + * <p> + * LayoutManager may pre-cache some views that are not necessarily visible. Those views + * are ignored in this method. + * + * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items. + * @see #findFirstCompletelyVisibleItemPosition() + * @see #findLastVisibleItemPosition() + */ + public int findFirstVisibleItemPosition() { + final View child = findOneVisibleChild(0, getChildCount(), false, true); + return child == null ? NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the first fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * <p> + * Note that bounds check is only performed in the current orientation. That means, if + * LayoutManager is horizontal, it will only check the view's left and right edges. + * + * @return The adapter position of the first fully visible item or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + * @see #findFirstVisibleItemPosition() + * @see #findLastCompletelyVisibleItemPosition() + */ + public int findFirstCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(0, getChildCount(), true, false); + return child == null ? NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the last visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * <p> + * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + * <p> + * If RecyclerView has item decorators, they will be considered in calculations as well. + * <p> + * LayoutManager may pre-cache some views that are not necessarily visible. Those views + * are ignored in this method. + * + * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items. + * @see #findLastCompletelyVisibleItemPosition() + * @see #findFirstVisibleItemPosition() + */ + public int findLastVisibleItemPosition() { + final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true); + return child == null ? NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the last fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * <p> + * Note that bounds check is only performed in the current orientation. That means, if + * LayoutManager is horizontal, it will only check the view's left and right edges. + * + * @return The adapter position of the last fully visible view or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + * @see #findLastVisibleItemPosition() + * @see #findFirstCompletelyVisibleItemPosition() + */ + public int findLastCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false); + return child == null ? NO_POSITION : getPosition(child); + } + + View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, + boolean acceptPartiallyVisible) { + ensureLayoutState(); + final int start = mOrientationHelper.getStartAfterPadding(); + final int end = mOrientationHelper.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; + View partiallyVisible = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = getChildAt(i); + final int childStart = mOrientationHelper.getDecoratedStart(child); + final int childEnd = mOrientationHelper.getDecoratedEnd(child); + if (childStart < end && childEnd > start) { + if (completelyVisible) { + if (childStart >= start && childEnd <= end) { + return child; + } else if (acceptPartiallyVisible && partiallyVisible == null) { + partiallyVisible = child; + } + } else { + return child; + } + } + } + return partiallyVisible; + } + + @Override + public View onFocusSearchFailed(View focused, int focusDirection, + RecyclerView.Recycler recycler, RecyclerView.State state) { + resolveShouldLayoutReverse(); + if (getChildCount() == 0) { + return null; + } + + final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); + if (layoutDir == LayoutState.INVALID_LAYOUT) { + return null; + } + ensureLayoutState(); + final View referenceChild; + if (layoutDir == LayoutState.LAYOUT_START) { + referenceChild = findReferenceChildClosestToStart(recycler, state); + } else { + referenceChild = findReferenceChildClosestToEnd(recycler, state); + } + if (referenceChild == null) { + if (DEBUG) { + Log.d(TAG, + "Cannot find a child with a valid position to be used for focus search."); + } + return null; + } + ensureLayoutState(); + final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); + updateLayoutState(layoutDir, maxScroll, false, state); + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + mLayoutState.mRecycle = false; + fill(recycler, mLayoutState, state, true); + final View nextFocus; + if (layoutDir == LayoutState.LAYOUT_START) { + nextFocus = getChildClosestToStart(); + } else { + nextFocus = getChildClosestToEnd(); + } + if (nextFocus == referenceChild || !nextFocus.isFocusable()) { + return null; + } + return nextFocus; + } + + /** + * Used for debugging. + * Logs the internal representation of children to default logger. + */ + private void logChildren() { + Log.d(TAG, "internal representation of views on the screen"); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + Log.d(TAG, "item " + getPosition(child) + ", coord:" + + mOrientationHelper.getDecoratedStart(child)); + } + Log.d(TAG, "=============="); + } + + /** + * Used for debugging. + * Validates that child views are laid out in correct order. This is important because rest of + * the algorithm relies on this constraint. + * + * In default layout, child 0 should be closest to screen position 0 and last child should be + * closest to position WIDTH or HEIGHT. + * In reverse layout, last child should be closes to screen position 0 and first child should + * be closest to position WIDTH or HEIGHT + */ + void validateChildOrder() { + Log.d(TAG, "validating child count " + getChildCount()); + if (getChildCount() < 1) { + return; + } + int lastPos = getPosition(getChildAt(0)); + int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0)); + if (mShouldReverseLayout) { + for (int i = 1; i < getChildCount(); i++) { + View child = getChildAt(i); + int pos = getPosition(child); + int screenLoc = mOrientationHelper.getDecoratedStart(child); + if (pos < lastPos) { + logChildren(); + throw new RuntimeException("detected invalid position. loc invalid? " + + (screenLoc < lastScreenLoc)); + } + if (screenLoc > lastScreenLoc) { + logChildren(); + throw new RuntimeException("detected invalid location"); + } + } + } else { + for (int i = 1; i < getChildCount(); i++) { + View child = getChildAt(i); + int pos = getPosition(child); + int screenLoc = mOrientationHelper.getDecoratedStart(child); + if (pos < lastPos) { + logChildren(); + throw new RuntimeException("detected invalid position. loc invalid? " + + (screenLoc < lastScreenLoc)); + } + if (screenLoc < lastScreenLoc) { + logChildren(); + throw new RuntimeException("detected invalid location"); + } + } + } + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd; + } + + /** + * @hide This method should be called by ItemTouchHelper only. + */ + @Override + public void prepareForDrop(View view, View target, int x, int y) { + assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation"); + ensureLayoutState(); + resolveShouldLayoutReverse(); + final int myPos = getPosition(view); + final int targetPos = getPosition(target); + final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + if (mShouldReverseLayout) { + if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() + - (mOrientationHelper.getDecoratedStart(target) + + mOrientationHelper.getDecoratedMeasurement(view))); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(target)); + } + } else { + if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) { + scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target)); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getDecoratedEnd(target) + - mOrientationHelper.getDecoratedMeasurement(view)); + } + } + } + + /** + * Helper class that keeps temporary state while {LayoutManager} is filling out the empty + * space. + */ + static class LayoutState { + + static final String TAG = "LLM#LayoutState"; + + static final int LAYOUT_START = -1; + + static final int LAYOUT_END = 1; + + static final int INVALID_LAYOUT = Integer.MIN_VALUE; + + static final int ITEM_DIRECTION_HEAD = -1; + + static final int ITEM_DIRECTION_TAIL = 1; + + static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE; + + /** + * We may not want to recycle children in some cases (e.g. layout) + */ + boolean mRecycle = true; + + /** + * Pixel offset where layout should start + */ + int mOffset; + + /** + * Number of pixels that we should fill, in the layout direction. + */ + int mAvailable; + + /** + * Current position on the adapter to get the next item. + */ + int mCurrentPosition; + + /** + * Defines the direction in which the data adapter is traversed. + * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} + */ + int mItemDirection; + + /** + * Defines the direction in which the layout is filled. + * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} + */ + int mLayoutDirection; + + /** + * Used when LayoutState is constructed in a scrolling state. + * It should be set the amount of scrolling we can make without creating a new view. + * Settings this is required for efficient view recycling. + */ + int mScrollingOffset; + + /** + * Used if you want to pre-layout items that are not yet visible. + * The difference with {@link #mAvailable} is that, when recycling, distance laid out for + * {@link #mExtra} is not considered to avoid recycling visible children. + */ + int mExtra = 0; + + /** + * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value + * is set to true, we skip removed views since they should not be laid out in post layout + * step. + */ + boolean mIsPreLayout = false; + + /** + * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)} + * amount. + */ + int mLastScrollDelta; + + /** + * When LLM needs to layout particular views, it sets this list in which case, LayoutState + * will only return views from this list and return null if it cannot find an item. + */ + List<RecyclerView.ViewHolder> mScrapList = null; + + /** + * Used when there is no limit in how many views can be laid out. + */ + boolean mInfinite; + + /** + * @return true if there are more items in the data adapter + */ + boolean hasMore(RecyclerView.State state) { + return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); + } + + /** + * Gets the view for the next element that we should layout. + * Also updates current item index to the next item, based on {@link #mItemDirection} + * + * @return The next element that we should layout. + */ + View next(RecyclerView.Recycler recycler) { + if (mScrapList != null) { + return nextViewFromScrapList(); + } + final View view = recycler.getViewForPosition(mCurrentPosition); + mCurrentPosition += mItemDirection; + return view; + } + + /** + * Returns the next item from the scrap list. + * <p> + * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection + * + * @return View if an item in the current position or direction exists if not null. + */ + private View nextViewFromScrapList() { + final int size = mScrapList.size(); + for (int i = 0; i < size; i++) { + final View view = mScrapList.get(i).itemView; + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.isItemRemoved()) { + continue; + } + if (mCurrentPosition == lp.getViewLayoutPosition()) { + assignPositionFromScrapList(view); + return view; + } + } + return null; + } + + public void assignPositionFromScrapList() { + assignPositionFromScrapList(null); + } + + public void assignPositionFromScrapList(View ignore) { + final View closest = nextViewInLimitedList(ignore); + if (closest == null) { + mCurrentPosition = NO_POSITION; + } else { + mCurrentPosition = ((LayoutParams) closest.getLayoutParams()) + .getViewLayoutPosition(); + } + } + + public View nextViewInLimitedList(View ignore) { + int size = mScrapList.size(); + View closest = null; + int closestDistance = Integer.MAX_VALUE; + if (DEBUG && mIsPreLayout) { + throw new IllegalStateException("Scrap list cannot be used in pre layout"); + } + for (int i = 0; i < size; i++) { + View view = mScrapList.get(i).itemView; + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (view == ignore || lp.isItemRemoved()) { + continue; + } + final int distance = (lp.getViewLayoutPosition() - mCurrentPosition) + * mItemDirection; + if (distance < 0) { + continue; // item is not in current direction + } + if (distance < closestDistance) { + closest = view; + closestDistance = distance; + if (distance == 0) { + break; + } + } + } + return closest; + } + + void log() { + Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); + } + } + + /** + * @hide + */ + public static class SavedState implements Parcelable { + + int mAnchorPosition; + + int mAnchorOffset; + + boolean mAnchorLayoutFromEnd; + + public SavedState() { + + } + + SavedState(Parcel in) { + mAnchorPosition = in.readInt(); + mAnchorOffset = in.readInt(); + mAnchorLayoutFromEnd = in.readInt() == 1; + } + + public SavedState(SavedState other) { + mAnchorPosition = other.mAnchorPosition; + mAnchorOffset = other.mAnchorOffset; + mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; + } + + boolean hasValidAnchor() { + return mAnchorPosition >= 0; + } + + void invalidateAnchor() { + mAnchorPosition = NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorPosition); + dest.writeInt(mAnchorOffset); + dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Simple data class to keep Anchor information + */ + class AnchorInfo { + int mPosition; + int mCoordinate; + boolean mLayoutFromEnd; + boolean mValid; + + AnchorInfo() { + reset(); + } + + void reset() { + mPosition = NO_POSITION; + mCoordinate = INVALID_OFFSET; + mLayoutFromEnd = false; + mValid = false; + } + + /** + * assigns anchor coordinate from the RecyclerView's padding depending on current + * layoutFromEnd value + */ + void assignCoordinateFromPadding() { + mCoordinate = mLayoutFromEnd + ? mOrientationHelper.getEndAfterPadding() + : mOrientationHelper.getStartAfterPadding(); + } + + @Override + public String toString() { + return "AnchorInfo{" + + "mPosition=" + mPosition + + ", mCoordinate=" + mCoordinate + + ", mLayoutFromEnd=" + mLayoutFromEnd + + ", mValid=" + mValid + + '}'; + } + + boolean isViewValidAsAnchor(View child, RecyclerView.State state) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0 + && lp.getViewLayoutPosition() < state.getItemCount(); + } + + public void assignFromViewAndKeepVisibleRect(View child) { + final int spaceChange = mOrientationHelper.getTotalSpaceChange(); + if (spaceChange >= 0) { + assignFromView(child); + return; + } + mPosition = getPosition(child); + if (mLayoutFromEnd) { + final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; + final int childEnd = mOrientationHelper.getDecoratedEnd(child); + final int previousEndMargin = prevLayoutEnd - childEnd; + mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin; + // ensure we did not push child's top out of bounds because of this + if (previousEndMargin > 0) { // we have room to shift bottom if necessary + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); + final int estimatedChildStart = mCoordinate - childSize; + final int layoutStart = mOrientationHelper.getStartAfterPadding(); + final int previousStartMargin = mOrientationHelper.getDecoratedStart(child) + - layoutStart; + final int startReference = layoutStart + Math.min(previousStartMargin, 0); + final int startMargin = estimatedChildStart - startReference; + if (startMargin < 0) { + // offset to make top visible but not too much + mCoordinate += Math.min(previousEndMargin, -startMargin); + } + } + } else { + final int childStart = mOrientationHelper.getDecoratedStart(child); + final int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); + mCoordinate = childStart; + if (startMargin > 0) { // we have room to fix end as well + final int estimatedEnd = childStart + + mOrientationHelper.getDecoratedMeasurement(child); + final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() + - spaceChange; + final int previousEndMargin = previousLayoutEnd + - mOrientationHelper.getDecoratedEnd(child); + final int endReference = mOrientationHelper.getEndAfterPadding() + - Math.min(0, previousEndMargin); + final int endMargin = endReference - estimatedEnd; + if (endMargin < 0) { + mCoordinate -= Math.min(startMargin, -endMargin); + } + } + } + } + + public void assignFromView(View child) { + if (mLayoutFromEnd) { + mCoordinate = mOrientationHelper.getDecoratedEnd(child) + + mOrientationHelper.getTotalSpaceChange(); + } else { + mCoordinate = mOrientationHelper.getDecoratedStart(child); + } + + mPosition = getPosition(child); + } + } + + protected static class LayoutChunkResult { + public int mConsumed; + public boolean mFinished; + public boolean mIgnoreConsumed; + public boolean mFocusable; + + void resetInternal() { + mConsumed = 0; + mFinished = false; + mIgnoreConsumed = false; + mFocusable = false; + } + } +} diff --git a/core/java/com/android/internal/widget/LinearSmoothScroller.java b/core/java/com/android/internal/widget/LinearSmoothScroller.java new file mode 100644 index 000000000000..d024f21a4cd0 --- /dev/null +++ b/core/java/com/android/internal/widget/LinearSmoothScroller.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.PointF; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +/** + * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until + * the target position becomes a child of the RecyclerView and then uses a + * {@link DecelerateInterpolator} to slowly approach to target position. + * <p> + * If the {@link RecyclerView.LayoutManager} you are using does not implement the + * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the + * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with + * the support library implement this interface. + */ +public class LinearSmoothScroller extends RecyclerView.SmoothScroller { + + private static final String TAG = "LinearSmoothScroller"; + + private static final boolean DEBUG = false; + + private static final float MILLISECONDS_PER_INCH = 25f; + + private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000; + + /** + * Align child view's left or top with parent view's left or top + * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_START = -1; + + /** + * Align child view's right or bottom with parent view's right or bottom + * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_END = 1; + + /** + * <p>Decides if the child should be snapped from start or end, depending on where it + * currently is in relation to its parent.</p> + * <p>For instance, if the view is virtually on the left of RecyclerView, using + * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p> + * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_ANY = 0; + + // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target + // view is not laid out until interim target position is reached, we can detect the case before + // scrolling slows down and reschedule another interim target scroll + private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f; + + protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator(); + + protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); + + protected PointF mTargetVector; + + private final float MILLISECONDS_PER_PX; + + // Temporary variables to keep track of the interim scroll target. These values do not + // point to a real item position, rather point to an estimated location pixels. + protected int mInterimTargetDx = 0, mInterimTargetDy = 0; + + public LinearSmoothScroller(Context context) { + MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onStart() { + + } + + /** + * {@inheritDoc} + */ + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference()); + final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); + final int distance = (int) Math.sqrt(dx * dx + dy * dy); + final int time = calculateTimeForDeceleration(distance); + if (time > 0) { + action.update(-dx, -dy, time, mDecelerateInterpolator); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { + if (getChildCount() == 0) { + stop(); + return; + } + //noinspection PointlessBooleanExpression + if (DEBUG && mTargetVector != null + && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) { + throw new IllegalStateException("Scroll happened in the opposite direction" + + " of the target. Some calculations are wrong"); + } + mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx); + mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy); + + if (mInterimTargetDx == 0 && mInterimTargetDy == 0) { + updateActionForInterimTarget(action); + } // everything is valid, keep going + + } + + /** + * {@inheritDoc} + */ + @Override + protected void onStop() { + mInterimTargetDx = mInterimTargetDy = 0; + mTargetVector = null; + } + + /** + * Calculates the scroll speed. + * + * @param displayMetrics DisplayMetrics to be used for real dimension calculations + * @return The time (in ms) it should take for each pixel. For instance, if returned value is + * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds. + */ + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + /** + * <p>Calculates the time for deceleration so that transition from LinearInterpolator to + * DecelerateInterpolator looks smooth.</p> + * + * @param dx Distance to scroll + * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning + * from LinearInterpolation + */ + protected int calculateTimeForDeceleration(int dx) { + // we want to cover same area with the linear interpolator for the first 10% of the + // interpolation. After that, deceleration will take control. + // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x + // which gives 0.100028 when x = .3356 + // this is why we divide linear scrolling time with .3356 + return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356); + } + + /** + * Calculates the time it should take to scroll the given distance (in pixels) + * + * @param dx Distance in pixels that we want to scroll + * @return Time in milliseconds + * @see #calculateSpeedPerPixel(android.util.DisplayMetrics) + */ + protected int calculateTimeForScrolling(int dx) { + // In a case where dx is very small, rounding may return 0 although dx > 0. + // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive + // time. + return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX); + } + + /** + * When scrolling towards a child view, this method defines whether we should align the left + * or the right edge of the child with the parent RecyclerView. + * + * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector + * @see #SNAP_TO_START + * @see #SNAP_TO_END + * @see #SNAP_TO_ANY + */ + protected int getHorizontalSnapPreference() { + return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY : + mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START; + } + + /** + * When scrolling towards a child view, this method defines whether we should align the top + * or the bottom edge of the child with the parent RecyclerView. + * + * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector + * @see #SNAP_TO_START + * @see #SNAP_TO_END + * @see #SNAP_TO_ANY + */ + protected int getVerticalSnapPreference() { + return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY : + mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START; + } + + /** + * When the target scroll position is not a child of the RecyclerView, this method calculates + * a direction vector towards that child and triggers a smooth scroll. + * + * @see #computeScrollVectorForPosition(int) + */ + protected void updateActionForInterimTarget(Action action) { + // find an interim target position + PointF scrollVector = computeScrollVectorForPosition(getTargetPosition()); + if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) { + final int target = getTargetPosition(); + action.jumpTo(target); + stop(); + return; + } + normalize(scrollVector); + mTargetVector = scrollVector; + + mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x); + mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y); + final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX); + // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the + // interim target. Since we track the distance travelled in onSeekTargetStep callback, it + // won't actually scroll more than what we need. + action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO), + (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO), + (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator); + } + + private int clampApplyScroll(int tmpDt, int dt) { + final int before = tmpDt; + tmpDt -= dt; + if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset + return 0; + } + return tmpDt; + } + + /** + * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and + * {@link #calculateDyToMakeVisible(android.view.View, int)} + */ + public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int + snapPreference) { + switch (snapPreference) { + case SNAP_TO_START: + return boxStart - viewStart; + case SNAP_TO_END: + return boxEnd - viewEnd; + case SNAP_TO_ANY: + final int dtStart = boxStart - viewStart; + if (dtStart > 0) { + return dtStart; + } + final int dtEnd = boxEnd - viewEnd; + if (dtEnd < 0) { + return dtEnd; + } + break; + default: + throw new IllegalArgumentException("snap preference should be one of the" + + " constants defined in SmoothScroller, starting with SNAP_"); + } + return 0; + } + + /** + * Calculates the vertical scroll amount necessary to make the given view fully visible + * inside the RecyclerView. + * + * @param view The view which we want to make fully visible + * @param snapPreference The edge which the view should snap to when entering the visible + * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or + * {@link #SNAP_TO_ANY}. + * @return The vertical scroll amount necessary to make the view visible with the given + * snap preference. + */ + public int calculateDyToMakeVisible(View view, int snapPreference) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollVertically()) { + return 0; + } + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + final int top = layoutManager.getDecoratedTop(view) - params.topMargin; + final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin; + final int start = layoutManager.getPaddingTop(); + final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); + return calculateDtToFit(top, bottom, start, end, snapPreference); + } + + /** + * Calculates the horizontal scroll amount necessary to make the given view fully visible + * inside the RecyclerView. + * + * @param view The view which we want to make fully visible + * @param snapPreference The edge which the view should snap to when entering the visible + * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or + * {@link #SNAP_TO_END} + * @return The vertical scroll amount necessary to make the view visible with the given + * snap preference. + */ + public int calculateDxToMakeVisible(View view, int snapPreference) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollHorizontally()) { + return 0; + } + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin; + final int right = layoutManager.getDecoratedRight(view) + params.rightMargin; + final int start = layoutManager.getPaddingLeft(); + final int end = layoutManager.getWidth() - layoutManager.getPaddingRight(); + return calculateDtToFit(left, right, start, end, snapPreference); + } + + /** + * Compute the scroll vector for a given target position. + * <p> + * This method can return null if the layout manager cannot calculate a scroll vector + * for the given position (e.g. it has no current scroll position). + * + * @param targetPosition the position to which the scroller is scrolling + * + * @return the scroll vector for a given target position + */ + @Nullable + public PointF computeScrollVectorForPosition(int targetPosition) { + RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof ScrollVectorProvider) { + return ((ScrollVectorProvider) layoutManager) + .computeScrollVectorForPosition(targetPosition); + } + Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" + + " does not implement " + ScrollVectorProvider.class.getCanonicalName()); + return null; + } +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index a29882b497b4..ece5540443ce 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1314,7 +1314,7 @@ public class LockPatternUtils { } private boolean isDoNotAskCredentialsOnBootSet() { - return mDevicePolicyManager.getDoNotAskCredentialsOnBoot(); + return getDevicePolicyManager().getDoNotAskCredentialsOnBoot(); } private boolean shouldEncryptWithCredentials(boolean defaultValue) { diff --git a/core/java/com/android/internal/widget/NestedScrollingChild.java b/core/java/com/android/internal/widget/NestedScrollingChild.java new file mode 100644 index 000000000000..20285b5ff44d --- /dev/null +++ b/core/java/com/android/internal/widget/NestedScrollingChild.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +/** + * This interface should be implemented by {@link android.view.View View} subclasses that wish + * to support dispatching nested scrolling operations to a cooperating parent + * {@link android.view.ViewGroup ViewGroup}. + * + * <p>Classes implementing this interface should create a final instance of a + * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the + * <code>NestedScrollingChildHelper</code> methods of the same signature.</p> + * + * <p>Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.</p> + */ +public interface NestedScrollingChild { + /** + * Enable or disable nested scrolling for this view. + * + * <p>If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.</p> + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled() + */ + void setNestedScrollingEnabled(boolean enabled); + + /** + * Returns true if nested scrolling is enabled for this view. + * + * <p>If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.</p> + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(boolean) + */ + boolean isNestedScrollingEnabled(); + + /** + * Begin a nestable scroll operation along the given axes. + * + * <p>A view starting a nested scroll promises to abide by the following contract:</p> + * + * <p>The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p> + * + * <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.</p> + * + * <p>At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.</p> + * + * <p>After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + * </p> + * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll() + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + boolean startNestedScroll(int axes); + + /** + * Stop a nested scroll in progress. + * + * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p> + * + * @see #startNestedScroll(int) + */ + void stopNestedScroll(); + + /** + * Returns true if this view has a nested scrolling parent. + * + * <p>The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p> + * + * @return whether this view has a nested scrolling parent + */ + boolean hasNestedScrollingParent(); + + /** + * Dispatch one step of a nested scroll in progress. + * + * <p>Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p> + * + * <p>Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.</p> + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + */ + boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.</p> + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); + + /** + * Dispatch a fling to a nested scrolling parent. + * + * <p>This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.</p> + * + * <p>If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.</p> + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + * <p>Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. <code>dispatchNestedPreFling</code> + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns <code>true</code>, a nested + * parent view consumed the fling and this view should not scroll as a result.</p> + * + * <p>For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:</p> + * + * <ul> + * <li>If a custom view is paged and needs to settle to a fixed page-point, do not + * call <code>dispatchNestedPreFling</code>; consume the fling and settle to a valid + * position regardless.</li> + * <li>If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.</li> + * </ul> + * + * <p>Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.</p> + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + boolean dispatchNestedPreFling(float velocityX, float velocityY); +} diff --git a/core/java/com/android/internal/widget/OpReorderer.java b/core/java/com/android/internal/widget/OpReorderer.java new file mode 100644 index 000000000000..babb0875b598 --- /dev/null +++ b/core/java/com/android/internal/widget/OpReorderer.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import static com.android.internal.widget.AdapterHelper.UpdateOp.ADD; +import static com.android.internal.widget.AdapterHelper.UpdateOp.MOVE; +import static com.android.internal.widget.AdapterHelper.UpdateOp.REMOVE; +import static com.android.internal.widget.AdapterHelper.UpdateOp.UPDATE; + +import com.android.internal.widget.AdapterHelper.UpdateOp; + +import java.util.List; + +class OpReorderer { + + final Callback mCallback; + + OpReorderer(Callback callback) { + mCallback = callback; + } + + void reorderOps(List<UpdateOp> ops) { + // since move operations breaks continuity, their effects on ADD/RM are hard to handle. + // we push them to the end of the list so that they can be handled easily. + int badMove; + while ((badMove = getLastMoveOutOfOrder(ops)) != -1) { + swapMoveOp(ops, badMove, badMove + 1); + } + } + + private void swapMoveOp(List<UpdateOp> list, int badMove, int next) { + final UpdateOp moveOp = list.get(badMove); + final UpdateOp nextOp = list.get(next); + switch (nextOp.cmd) { + case REMOVE: + swapMoveRemove(list, badMove, moveOp, next, nextOp); + break; + case ADD: + swapMoveAdd(list, badMove, moveOp, next, nextOp); + break; + case UPDATE: + swapMoveUpdate(list, badMove, moveOp, next, nextOp); + break; + } + } + + void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp, + int removePos, UpdateOp removeOp) { + UpdateOp extraRm = null; + // check if move is nulled out by remove + boolean revertedMove = false; + final boolean moveIsBackwards; + + if (moveOp.positionStart < moveOp.itemCount) { + moveIsBackwards = false; + if (removeOp.positionStart == moveOp.positionStart + && removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) { + revertedMove = true; + } + } else { + moveIsBackwards = true; + if (removeOp.positionStart == moveOp.itemCount + 1 + && removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) { + revertedMove = true; + } + } + + // going in reverse, first revert the effect of add + if (moveOp.itemCount < removeOp.positionStart) { + removeOp.positionStart--; + } else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) { + // move is removed. + removeOp.itemCount--; + moveOp.cmd = REMOVE; + moveOp.itemCount = 1; + if (removeOp.itemCount == 0) { + list.remove(removePos); + mCallback.recycleUpdateOp(removeOp); + } + // no need to swap, it is already a remove + return; + } + + // now affect of add is consumed. now apply effect of first remove + if (moveOp.positionStart <= removeOp.positionStart) { + removeOp.positionStart++; + } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) { + final int remaining = removeOp.positionStart + removeOp.itemCount + - moveOp.positionStart; + extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null); + removeOp.itemCount = moveOp.positionStart - removeOp.positionStart; + } + + // if effects of move is reverted by remove, we are done. + if (revertedMove) { + list.set(movePos, removeOp); + list.remove(removePos); + mCallback.recycleUpdateOp(moveOp); + return; + } + + // now find out the new locations for move actions + if (moveIsBackwards) { + if (extraRm != null) { + if (moveOp.positionStart > extraRm.positionStart) { + moveOp.positionStart -= extraRm.itemCount; + } + if (moveOp.itemCount > extraRm.positionStart) { + moveOp.itemCount -= extraRm.itemCount; + } + } + if (moveOp.positionStart > removeOp.positionStart) { + moveOp.positionStart -= removeOp.itemCount; + } + if (moveOp.itemCount > removeOp.positionStart) { + moveOp.itemCount -= removeOp.itemCount; + } + } else { + if (extraRm != null) { + if (moveOp.positionStart >= extraRm.positionStart) { + moveOp.positionStart -= extraRm.itemCount; + } + if (moveOp.itemCount >= extraRm.positionStart) { + moveOp.itemCount -= extraRm.itemCount; + } + } + if (moveOp.positionStart >= removeOp.positionStart) { + moveOp.positionStart -= removeOp.itemCount; + } + if (moveOp.itemCount >= removeOp.positionStart) { + moveOp.itemCount -= removeOp.itemCount; + } + } + + list.set(movePos, removeOp); + if (moveOp.positionStart != moveOp.itemCount) { + list.set(removePos, moveOp); + } else { + list.remove(removePos); + } + if (extraRm != null) { + list.add(movePos, extraRm); + } + } + + private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add, + UpdateOp addOp) { + int offset = 0; + // going in reverse, first revert the effect of add + if (moveOp.itemCount < addOp.positionStart) { + offset--; + } + if (moveOp.positionStart < addOp.positionStart) { + offset++; + } + if (addOp.positionStart <= moveOp.positionStart) { + moveOp.positionStart += addOp.itemCount; + } + if (addOp.positionStart <= moveOp.itemCount) { + moveOp.itemCount += addOp.itemCount; + } + addOp.positionStart += offset; + list.set(move, addOp); + list.set(add, moveOp); + } + + void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update, + UpdateOp updateOp) { + UpdateOp extraUp1 = null; + UpdateOp extraUp2 = null; + // going in reverse, first revert the effect of add + if (moveOp.itemCount < updateOp.positionStart) { + updateOp.positionStart--; + } else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) { + // moved item is updated. add an update for it + updateOp.itemCount--; + extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload); + } + // now affect of add is consumed. now apply effect of first remove + if (moveOp.positionStart <= updateOp.positionStart) { + updateOp.positionStart++; + } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) { + final int remaining = updateOp.positionStart + updateOp.itemCount + - moveOp.positionStart; + extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining, + updateOp.payload); + updateOp.itemCount -= remaining; + } + list.set(update, moveOp); + if (updateOp.itemCount > 0) { + list.set(move, updateOp); + } else { + list.remove(move); + mCallback.recycleUpdateOp(updateOp); + } + if (extraUp1 != null) { + list.add(move, extraUp1); + } + if (extraUp2 != null) { + list.add(move, extraUp2); + } + } + + private int getLastMoveOutOfOrder(List<UpdateOp> list) { + boolean foundNonMove = false; + for (int i = list.size() - 1; i >= 0; i--) { + final UpdateOp op1 = list.get(i); + if (op1.cmd == MOVE) { + if (foundNonMove) { + return i; + } + } else { + foundNonMove = true; + } + } + return -1; + } + + interface Callback { + + UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload); + + void recycleUpdateOp(UpdateOp op); + } +} diff --git a/core/java/com/android/internal/widget/OrientationHelper.java b/core/java/com/android/internal/widget/OrientationHelper.java new file mode 100644 index 000000000000..1b02c886f56f --- /dev/null +++ b/core/java/com/android/internal/widget/OrientationHelper.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.graphics.Rect; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Helper class for LayoutManagers to abstract measurements depending on the View's orientation. + * <p> + * It is developed to easily support vertical and horizontal orientations in a LayoutManager but + * can also be used to abstract calls around view bounds and child measurements with margins and + * decorations. + * + * @see #createHorizontalHelper(RecyclerView.LayoutManager) + * @see #createVerticalHelper(RecyclerView.LayoutManager) + */ +public abstract class OrientationHelper { + + private static final int INVALID_SIZE = Integer.MIN_VALUE; + + protected final RecyclerView.LayoutManager mLayoutManager; + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + + public static final int VERTICAL = LinearLayout.VERTICAL; + + private int mLastTotalSpace = INVALID_SIZE; + + final Rect mTmpRect = new Rect(); + + private OrientationHelper(RecyclerView.LayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + /** + * Call this method after onLayout method is complete if state is NOT pre-layout. + * This method records information like layout bounds that might be useful in the next layout + * calculations. + */ + public void onLayoutComplete() { + mLastTotalSpace = getTotalSpace(); + } + + /** + * Returns the layout space change between the previous layout pass and current layout pass. + * <p> + * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's + * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, + * RecyclerView.State)} method. + * + * @return The difference between the current total space and previous layout's total space. + * @see #onLayoutComplete() + */ + public int getTotalSpaceChange() { + return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace; + } + + /** + * Returns the start of the view including its decoration and margin. + * <p> + * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left + * decoration and 3px left margin, returned value will be 15px. + * + * @param view The view element to check + * @return The first pixel of the element + * @see #getDecoratedEnd(android.view.View) + */ + public abstract int getDecoratedStart(View view); + + /** + * Returns the end of the view including its decoration and margin. + * <p> + * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right + * decoration and 3px right margin, returned value will be 205. + * + * @param view The view element to check + * @return The last pixel of the element + * @see #getDecoratedStart(android.view.View) + */ + public abstract int getDecoratedEnd(View view); + + /** + * Returns the end of the View after its matrix transformations are applied to its layout + * position. + * <p> + * This method is useful when trying to detect the visible edge of a View. + * <p> + * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed end will be returned + * @return The end of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedEndWithDecoration(View view); + + /** + * Returns the start of the View after its matrix transformations are applied to its layout + * position. + * <p> + * This method is useful when trying to detect the visible edge of a View. + * <p> + * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed start will be returned + * @return The start of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedStartWithDecoration(View view); + + /** + * Returns the space occupied by this View in the current orientation including decorations and + * margins. + * + * @param view The view element to check + * @return Total space occupied by this view + * @see #getDecoratedMeasurementInOther(View) + */ + public abstract int getDecoratedMeasurement(View view); + + /** + * Returns the space occupied by this View in the perpendicular orientation including + * decorations and margins. + * + * @param view The view element to check + * @return Total space occupied by this view in the perpendicular orientation to current one + * @see #getDecoratedMeasurement(View) + */ + public abstract int getDecoratedMeasurementInOther(View view); + + /** + * Returns the start position of the layout after the start padding is added. + * + * @return The very first pixel we can draw. + */ + public abstract int getStartAfterPadding(); + + /** + * Returns the end position of the layout after the end padding is removed. + * + * @return The end boundary for this layout. + */ + public abstract int getEndAfterPadding(); + + /** + * Returns the end position of the layout without taking padding into account. + * + * @return The end boundary for this layout without considering padding. + */ + public abstract int getEnd(); + + /** + * Offsets all children's positions by the given amount. + * + * @param amount Value to add to each child's layout parameters + */ + public abstract void offsetChildren(int amount); + + /** + * Returns the total space to layout. This number is the difference between + * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}. + * + * @return Total space to layout children + */ + public abstract int getTotalSpace(); + + /** + * Offsets the child in this orientation. + * + * @param view View to offset + * @param offset offset amount + */ + public abstract void offsetChild(View view, int offset); + + /** + * Returns the padding at the end of the layout. For horizontal helper, this is the right + * padding and for vertical helper, this is the bottom padding. This method does not check + * whether the layout is RTL or not. + * + * @return The padding at the end of the layout. + */ + public abstract int getEndPadding(); + + /** + * Returns the MeasureSpec mode for the current orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getMode(); + + /** + * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getModeInOther(); + + /** + * Creates an OrientationHelper for the given LayoutManager and orientation. + * + * @param layoutManager LayoutManager to attach to + * @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL} + * @return A new OrientationHelper + */ + public static OrientationHelper createOrientationHelper( + RecyclerView.LayoutManager layoutManager, int orientation) { + switch (orientation) { + case HORIZONTAL: + return createHorizontalHelper(layoutManager); + case VERTICAL: + return createVerticalHelper(layoutManager); + } + throw new IllegalArgumentException("invalid orientation"); + } + + /** + * Creates a horizontal OrientationHelper for the given LayoutManager. + * + * @param layoutManager The LayoutManager to attach to. + * @return A new OrientationHelper + */ + public static OrientationHelper createHorizontalHelper( + RecyclerView.LayoutManager layoutManager) { + return new OrientationHelper(layoutManager) { + @Override + public int getEndAfterPadding() { + return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight(); + } + + @Override + public int getEnd() { + return mLayoutManager.getWidth(); + } + + @Override + public void offsetChildren(int amount) { + mLayoutManager.offsetChildrenHorizontal(amount); + } + + @Override + public int getStartAfterPadding() { + return mLayoutManager.getPaddingLeft(); + } + + @Override + public int getDecoratedMeasurement(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + + params.rightMargin; + } + + @Override + public int getDecoratedMeasurementInOther(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + + params.bottomMargin; + } + + @Override + public int getDecoratedEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedRight(view) + params.rightMargin; + } + + @Override + public int getDecoratedStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedLeft(view) - params.leftMargin; + } + + @Override + public int getTransformedEndWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.right; + } + + @Override + public int getTransformedStartWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.left; + } + + @Override + public int getTotalSpace() { + return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft() + - mLayoutManager.getPaddingRight(); + } + + @Override + public void offsetChild(View view, int offset) { + view.offsetLeftAndRight(offset); + } + + @Override + public int getEndPadding() { + return mLayoutManager.getPaddingRight(); + } + + @Override + public int getMode() { + return mLayoutManager.getWidthMode(); + } + + @Override + public int getModeInOther() { + return mLayoutManager.getHeightMode(); + } + }; + } + + /** + * Creates a vertical OrientationHelper for the given LayoutManager. + * + * @param layoutManager The LayoutManager to attach to. + * @return A new OrientationHelper + */ + public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) { + return new OrientationHelper(layoutManager) { + @Override + public int getEndAfterPadding() { + return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom(); + } + + @Override + public int getEnd() { + return mLayoutManager.getHeight(); + } + + @Override + public void offsetChildren(int amount) { + mLayoutManager.offsetChildrenVertical(amount); + } + + @Override + public int getStartAfterPadding() { + return mLayoutManager.getPaddingTop(); + } + + @Override + public int getDecoratedMeasurement(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + + params.bottomMargin; + } + + @Override + public int getDecoratedMeasurementInOther(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + + params.rightMargin; + } + + @Override + public int getDecoratedEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin; + } + + @Override + public int getDecoratedStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedTop(view) - params.topMargin; + } + + @Override + public int getTransformedEndWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.bottom; + } + + @Override + public int getTransformedStartWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.top; + } + + @Override + public int getTotalSpace() { + return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop() + - mLayoutManager.getPaddingBottom(); + } + + @Override + public void offsetChild(View view, int offset) { + view.offsetTopAndBottom(offset); + } + + @Override + public int getEndPadding() { + return mLayoutManager.getPaddingBottom(); + } + + @Override + public int getMode() { + return mLayoutManager.getHeightMode(); + } + + @Override + public int getModeInOther() { + return mLayoutManager.getWidthMode(); + } + }; + } +} diff --git a/core/java/com/android/internal/widget/RecyclerView.java b/core/java/com/android/internal/widget/RecyclerView.java new file mode 100644 index 000000000000..0cf3164de98e --- /dev/null +++ b/core/java/com/android/internal/widget/RecyclerView.java @@ -0,0 +1,12255 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.CallSuper; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Observable; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.os.Trace; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.AbsSavedState; +import android.view.Display; +import android.view.FocusFinder; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.OverScroller; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A flexible view for providing a limited window into a large data set. + * + * <h3>Glossary of terms:</h3> + * + * <ul> + * <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views + * that represent items in a data set.</li> + * <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li> + * <li><em>Index:</em> The index of an attached child view as used in a call to + * {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li> + * <li><em>Binding:</em> The process of preparing a child view to display data corresponding + * to a <em>position</em> within the adapter.</li> + * <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter + * position may be placed in a cache for later reuse to display the same type of data again + * later. This can drastically improve performance by skipping initial layout inflation + * or construction.</li> + * <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached + * state during layout. Scrap views may be reused without becoming fully detached + * from the parent RecyclerView, either unmodified if no rebinding is required or modified + * by the adapter if the view was considered <em>dirty</em>.</li> + * <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before + * being displayed.</li> + * </ul> + * + * <h4>Positions in RecyclerView:</h4> + * <p> + * RecyclerView introduces an additional level of abstraction between the {@link Adapter} and + * {@link LayoutManager} to be able to detect data set changes in batches during a layout + * calculation. This saves LayoutManager from tracking adapter changes to calculate animations. + * It also helps with performance because all view bindings happen at the same time and unnecessary + * bindings are avoided. + * <p> + * For this reason, there are two types of <code>position</code> related methods in RecyclerView: + * <ul> + * <li>layout position: Position of an item in the latest layout calculation. This is the + * position from the LayoutManager's perspective.</li> + * <li>adapter position: Position of an item in the adapter. This is the position from + * the Adapter's perspective.</li> + * </ul> + * <p> + * These two positions are the same except the time between dispatching <code>adapter.notify* + * </code> events and calculating the updated layout. + * <p> + * Methods that return or receive <code>*LayoutPosition*</code> use position as of the latest + * layout calculation (e.g. {@link ViewHolder#getLayoutPosition()}, + * {@link #findViewHolderForLayoutPosition(int)}). These positions include all changes until the + * last layout calculation. You can rely on these positions to be consistent with what user is + * currently seeing on the screen. For example, if you have a list of items on the screen and user + * asks for the 5<sup>th</sup> element, you should use these methods as they'll match what user + * is seeing. + * <p> + * The other set of position related methods are in the form of + * <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAdapterPosition()}, + * {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to + * work with up-to-date adapter positions even if they may not have been reflected to layout yet. + * For example, if you want to access the item in the adapter on a ViewHolder click, you should use + * {@link ViewHolder#getAdapterPosition()}. Beware that these methods may not be able to calculate + * adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new layout has + * not yet been calculated. For this reasons, you should carefully handle {@link #NO_POSITION} or + * <code>null</code> results from these methods. + * <p> + * When writing a {@link LayoutManager} you almost always want to use layout positions whereas when + * writing an {@link Adapter}, you probably want to use adapter positions. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_layoutManager + */ +public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild { + + static final String TAG = "RecyclerView"; + + static final boolean DEBUG = false; + + private static final int[] NESTED_SCROLLING_ATTRS = { android.R.attr.nestedScrollingEnabled }; + + private static final int[] CLIP_TO_PADDING_ATTR = {android.R.attr.clipToPadding}; + + /** + * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if + * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by + * setting View's visibility to INVISIBLE when View is detached. On Kitkat and JB MR2, Recycler + * recursively traverses itemView and invalidates display list for each ViewGroup that matches + * this criteria. + */ + static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18 + || Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20; + /** + * On M+, an unspecified measure spec may include a hint which we can use. On older platforms, + * this value might be garbage. To save LayoutManagers from it, RecyclerView sets the size to + * 0 when mode is unspecified. + */ + static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23; + + static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16; + + /** + * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to + * RenderThread but before the next frame begins. We schedule prefetch work in this window. + */ + private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21; + + /** + * FocusFinder#findNextFocus is broken on ICS MR1 and older for View.FOCUS_BACKWARD direction. + * We convert it to an absolute direction such as FOCUS_DOWN or FOCUS_LEFT. + */ + private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15; + + /** + * on API 15-, a focused child can still be considered a focused child of RV even after + * it's being removed or its focusable flag is set to false. This is because when this focused + * child is detached, the reference to this child is not removed in clearFocus. API 16 and above + * properly handle this case by calling ensureInputFocusOnFirstFocusable or rootViewRequestFocus + * to request focus on a new child, which will clear the focus on the old (detached) child as a + * side-effect. + */ + private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15; + + static final boolean DISPATCH_TEMP_DETACH = false; + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public static final int NO_POSITION = -1; + public static final long NO_ID = -1; + public static final int INVALID_TYPE = -1; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for smooth, + * continuous scrolling. + */ + public static final int TOUCH_SLOP_DEFAULT = 0; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for scrolling + * widgets that snap to a page or other coarse-grained barrier. + */ + public static final int TOUCH_SLOP_PAGING = 1; + + static final int MAX_SCROLL_DURATION = 2000; + + /** + * RecyclerView is calculating a scroll. + * If there are too many of these in Systrace, some Views inside RecyclerView might be causing + * it. Try to avoid using EditText, focusable views or handle them with care. + */ + static final String TRACE_SCROLL_TAG = "RV Scroll"; + + /** + * OnLayout has been called by the View system. + * If this shows up too many times in Systrace, make sure the children of RecyclerView do not + * update themselves directly. This will cause a full re-layout but when it happens via the + * Adapter notifyItemChanged, RecyclerView can avoid full layout calculation. + */ + private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout"; + + /** + * NotifyDataSetChanged or equal has been called. + * If this is taking a long time, try sending granular notify adapter changes instead of just + * calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter + * might help. + */ + private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate"; + + /** + * RecyclerView is doing a layout for partial adapter updates (we know what has changed) + * If this is taking a long time, you may have dispatched too many Adapter updates causing too + * many Views being rebind. Make sure all are necessary and also prefer using notify*Range + * methods. + */ + private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate"; + + /** + * RecyclerView is rebinding a View. + * If this is taking a lot of time, consider optimizing your layout or make sure you are not + * doing extra operations in onBindViewHolder call. + */ + static final String TRACE_BIND_VIEW_TAG = "RV OnBindView"; + + /** + * RecyclerView is attempting to pre-populate off screen views. + */ + static final String TRACE_PREFETCH_TAG = "RV Prefetch"; + + /** + * RecyclerView is attempting to pre-populate off screen itemviews within an off screen + * RecyclerView. + */ + static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch"; + + /** + * RecyclerView is creating a new View. + * If too many of these present in Systrace: + * - There might be a problem in Recycling (e.g. custom Animations that set transient state and + * prevent recycling or ItemAnimator not implementing the contract properly. ({@link + * > Adapter#onFailedToRecycleView(ViewHolder)}) + * + * - There might be too many item view types. + * > Try merging them + * + * - There might be too many itemChange animations and not enough space in RecyclerPool. + * >Try increasing your pool size and item cache size. + */ + static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; + private static final Class<?>[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE = + new Class[]{Context.class, AttributeSet.class, int.class, int.class}; + + private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); + + final Recycler mRecycler = new Recycler(); + + private SavedState mPendingSavedState; + + /** + * Handles adapter updates + */ + AdapterHelper mAdapterHelper; + + /** + * Handles abstraction between LayoutManager children and RecyclerView children + */ + ChildHelper mChildHelper; + + /** + * Keeps data about views to be used for animations + */ + final ViewInfoStore mViewInfoStore = new ViewInfoStore(); + + /** + * Prior to L, there is no way to query this variable which is why we override the setter and + * track it here. + */ + boolean mClipToPadding; + + /** + * Note: this Runnable is only ever posted if: + * 1) We've been through first layout + * 2) We know we have a fixed size (mHasFixedSize) + * 3) We're attached + */ + final Runnable mUpdateChildViewsRunnable = new Runnable() { + @Override + public void run() { + if (!mFirstLayoutComplete || isLayoutRequested()) { + // a layout request will happen, we should not do layout here. + return; + } + if (!mIsAttached) { + requestLayout(); + // if we are not attached yet, mark us as requiring layout and skip + return; + } + if (mLayoutFrozen) { + mLayoutRequestEaten = true; + return; //we'll process updates when ice age ends. + } + consumePendingUpdateOperations(); + } + }; + + final Rect mTempRect = new Rect(); + private final Rect mTempRect2 = new Rect(); + final RectF mTempRectF = new RectF(); + Adapter mAdapter; + @VisibleForTesting LayoutManager mLayout; + RecyclerListener mRecyclerListener; + final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>(); + private final ArrayList<OnItemTouchListener> mOnItemTouchListeners = + new ArrayList<>(); + private OnItemTouchListener mActiveOnItemTouchListener; + boolean mIsAttached; + boolean mHasFixedSize; + @VisibleForTesting boolean mFirstLayoutComplete; + + // Counting lock to control whether we should ignore requestLayout calls from children or not. + private int mEatRequestLayout = 0; + + boolean mLayoutRequestEaten; + boolean mLayoutFrozen; + private boolean mIgnoreMotionEventTillDown; + + // binary OR of change events that were eaten during a layout or scroll. + private int mEatenAccessibilityChangeFlags; + boolean mAdapterUpdateDuringMeasure; + + private final AccessibilityManager mAccessibilityManager; + private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners; + + /** + * Set to true when an adapter data set changed notification is received. + * In that case, we cannot run any animations since we don't know what happened until layout. + * + * Attached items are invalid until next layout, at which point layout will animate/replace + * items as necessary, building up content from the (effectively) new adapter from scratch. + * + * Cached items must be discarded when setting this to true, so that the cache may be freely + * used by prefetching until the next layout occurs. + * + * @see #setDataSetChangedAfterLayout() + */ + boolean mDataSetHasChangedAfterLayout = false; + + /** + * This variable is incremented during a dispatchLayout and/or scroll. + * Some methods should not be called during these periods (e.g. adapter data change). + * Doing so will create hard to find bugs so we better check it and throw an exception. + * + * @see #assertInLayoutOrScroll(String) + * @see #assertNotInLayoutOrScroll(String) + */ + private int mLayoutOrScrollCounter = 0; + + /** + * Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception + * (for API compatibility). + * <p> + * It is a bad practice for a developer to update the data in a scroll callback since it is + * potentially called during a layout. + */ + private int mDispatchScrollCounter = 0; + + private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow; + + ItemAnimator mItemAnimator = new DefaultItemAnimator(); + + private static final int INVALID_POINTER = -1; + + /** + * The RecyclerView is not currently scrolling. + * @see #getScrollState() + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * The RecyclerView is currently being dragged by outside input such as user touch input. + * @see #getScrollState() + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * The RecyclerView is currently animating to a final position while not under + * outside control. + * @see #getScrollState() + */ + public static final int SCROLL_STATE_SETTLING = 2; + + static final long FOREVER_NS = Long.MAX_VALUE; + + // Touch/scrolling handling + + private int mScrollState = SCROLL_STATE_IDLE; + private int mScrollPointerId = INVALID_POINTER; + private VelocityTracker mVelocityTracker; + private int mInitialTouchX; + private int mInitialTouchY; + private int mLastTouchX; + private int mLastTouchY; + private int mTouchSlop; + private OnFlingListener mOnFlingListener; + private final int mMinFlingVelocity; + private final int mMaxFlingVelocity; + // This value is used when handling generic motion events. + private float mScrollFactor = Float.MIN_VALUE; + private boolean mPreserveFocusAfterLayout = true; + + final ViewFlinger mViewFlinger = new ViewFlinger(); + + GapWorker mGapWorker; + GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry = + ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null; + + final State mState = new State(); + + private OnScrollListener mScrollListener; + private List<OnScrollListener> mScrollListeners; + + // For use in item animations + boolean mItemsAddedOrRemoved = false; + boolean mItemsChanged = false; + private ItemAnimator.ItemAnimatorListener mItemAnimatorListener = + new ItemAnimatorRestoreListener(); + boolean mPostedAnimatorRunner = false; + RecyclerViewAccessibilityDelegate mAccessibilityDelegate; + private ChildDrawingOrderCallback mChildDrawingOrderCallback; + + // simple array to keep min and max child position during a layout calculation + // preserved not to create a new one in each layout pass + private final int[] mMinMaxLayoutPositions = new int[2]; + + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private final int[] mNestedOffsets = new int[2]; + + /** + * These are views that had their a11y importance changed during a layout. We defer these events + * until the end of the layout because a11y service may make sync calls back to the RV while + * the View's state is undefined. + */ + @VisibleForTesting + final List<ViewHolder> mPendingAccessibilityImportanceChange = new ArrayList(); + + private Runnable mItemAnimatorRunner = new Runnable() { + @Override + public void run() { + if (mItemAnimator != null) { + mItemAnimator.runPendingAnimations(); + } + mPostedAnimatorRunner = false; + } + }; + + static final Interpolator sQuinticInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + /** + * The callback to convert view info diffs into animations. + */ + private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = + new ViewInfoStore.ProcessCallback() { + @Override + public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, + @Nullable ItemHolderInfo postInfo) { + mRecycler.unscrapView(viewHolder); + animateDisappearance(viewHolder, info, postInfo); + } + @Override + public void processAppeared(ViewHolder viewHolder, + ItemHolderInfo preInfo, ItemHolderInfo info) { + animateAppearance(viewHolder, preInfo, info); + } + + @Override + public void processPersistent(ViewHolder viewHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + viewHolder.setIsRecyclable(false); + if (mDataSetHasChangedAfterLayout) { + // since it was rebound, use change instead as we'll be mapping them from + // stable ids. If stable ids were false, we would not be running any + // animations + if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + @Override + public void unused(ViewHolder viewHolder) { + mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); + } + }; + + public RecyclerView(Context context) { + this(context, null); + } + + public RecyclerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0); + mClipToPadding = a.getBoolean(0, true); + a.recycle(); + } else { + mClipToPadding = true; + } + setScrollContainer(true); + setFocusableInTouchMode(true); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); + + mItemAnimator.setListener(mItemAnimatorListener); + initAdapterManager(); + initChildrenHelper(); + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + mAccessibilityManager = (AccessibilityManager) getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); + // Create the layoutManager if specified. + + boolean nestedScrollingEnabled = true; + + if (attrs != null) { + int defStyleRes = 0; + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyle, defStyleRes); + String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager); + int descendantFocusability = a.getInt( + R.styleable.RecyclerView_descendantFocusability, -1); + if (descendantFocusability == -1) { + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + } + a.recycle(); + createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes); + + if (Build.VERSION.SDK_INT >= 21) { + a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, + defStyle, defStyleRes); + nestedScrollingEnabled = a.getBoolean(0, true); + a.recycle(); + } + } else { + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + } + + // Re-set whether nested scrolling is enabled so that it is set on all API levels + setNestedScrollingEnabled(nestedScrollingEnabled); + } + + /** + * Returns the accessibility delegate compatibility implementation used by the RecyclerView. + * @return An instance of AccessibilityDelegateCompat used by RecyclerView + */ + public RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate() { + return mAccessibilityDelegate; + } + + /** + * Sets the accessibility delegate compatibility implementation used by RecyclerView. + * @param accessibilityDelegate The accessibility delegate to be used by RecyclerView. + */ + public void setAccessibilityDelegateCompat( + RecyclerViewAccessibilityDelegate accessibilityDelegate) { + mAccessibilityDelegate = accessibilityDelegate; + setAccessibilityDelegate(mAccessibilityDelegate); + } + + /** + * Instantiate and set a LayoutManager, if specified in the attributes. + */ + private void createLayoutManager(Context context, String className, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + if (className != null) { + className = className.trim(); + if (className.length() != 0) { // Can't use isEmpty since it was added in API 9. + className = getFullClassName(context, className); + try { + ClassLoader classLoader; + if (isInEditMode()) { + // Stupid layoutlib cannot handle simple class loaders. + classLoader = this.getClass().getClassLoader(); + } else { + classLoader = context.getClassLoader(); + } + Class<? extends LayoutManager> layoutManagerClass = + classLoader.loadClass(className).asSubclass(LayoutManager.class); + Constructor<? extends LayoutManager> constructor; + Object[] constructorArgs = null; + try { + constructor = layoutManagerClass + .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE); + constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes}; + } catch (NoSuchMethodException e) { + try { + constructor = layoutManagerClass.getConstructor(); + } catch (NoSuchMethodException e1) { + e1.initCause(e); + throw new IllegalStateException(attrs.getPositionDescription() + + ": Error creating LayoutManager " + className, e1); + } + } + constructor.setAccessible(true); + setLayoutManager(constructor.newInstance(constructorArgs)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Unable to find LayoutManager " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (InstantiationException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Cannot access non-public constructor " + className, e); + } catch (ClassCastException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Class is not a LayoutManager " + className, e); + } + } + } + } + + private String getFullClassName(Context context, String className) { + if (className.charAt(0) == '.') { + return context.getPackageName() + className; + } + if (className.contains(".")) { + return className; + } + return RecyclerView.class.getPackage().getName() + '.' + className; + } + + private void initChildrenHelper() { + mChildHelper = new ChildHelper(new ChildHelper.Callback() { + @Override + public int getChildCount() { + return RecyclerView.this.getChildCount(); + } + + @Override + public void addView(View child, int index) { + RecyclerView.this.addView(child, index); + dispatchChildAttached(child); + } + + @Override + public int indexOfChild(View view) { + return RecyclerView.this.indexOfChild(view); + } + + @Override + public void removeViewAt(int index) { + final View child = RecyclerView.this.getChildAt(index); + if (child != null) { + dispatchChildDetached(child); + } + RecyclerView.this.removeViewAt(index); + } + + @Override + public View getChildAt(int offset) { + return RecyclerView.this.getChildAt(offset); + } + + @Override + public void removeAllViews() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + dispatchChildDetached(getChildAt(i)); + } + RecyclerView.this.removeAllViews(); + } + + @Override + public ViewHolder getChildViewHolder(View view) { + return getChildViewHolderInt(view); + } + + @Override + public void attachViewToParent(View child, int index, + ViewGroup.LayoutParams layoutParams) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + if (!vh.isTmpDetached() && !vh.shouldIgnore()) { + throw new IllegalArgumentException("Called attach on a child which is not" + + " detached: " + vh); + } + if (DEBUG) { + Log.d(TAG, "reAttach " + vh); + } + vh.clearTmpDetachFlag(); + } + RecyclerView.this.attachViewToParent(child, index, layoutParams); + } + + @Override + public void detachViewFromParent(int offset) { + final View view = getChildAt(offset); + if (view != null) { + final ViewHolder vh = getChildViewHolderInt(view); + if (vh != null) { + if (vh.isTmpDetached() && !vh.shouldIgnore()) { + throw new IllegalArgumentException("called detach on an already" + + " detached child " + vh); + } + if (DEBUG) { + Log.d(TAG, "tmpDetach " + vh); + } + vh.addFlags(ViewHolder.FLAG_TMP_DETACHED); + } + } + RecyclerView.this.detachViewFromParent(offset); + } + + @Override + public void onEnteredHiddenState(View child) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + vh.onEnteredHiddenState(RecyclerView.this); + } + } + + @Override + public void onLeftHiddenState(View child) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + vh.onLeftHiddenState(RecyclerView.this); + } + } + }); + } + + void initAdapterManager() { + mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() { + @Override + public ViewHolder findViewHolder(int position) { + final ViewHolder vh = findViewHolderForPosition(position, true); + if (vh == null) { + return null; + } + // ensure it is not hidden because for adapter helper, the only thing matter is that + // LM thinks view is a child. + if (mChildHelper.isHidden(vh.itemView)) { + if (DEBUG) { + Log.d(TAG, "assuming view holder cannot be find because it is hidden"); + } + return null; + } + return vh; + } + + @Override + public void offsetPositionsForRemovingInvisible(int start, int count) { + offsetPositionRecordsForRemove(start, count, true); + mItemsAddedOrRemoved = true; + mState.mDeletedInvisibleItemCountSincePreviousLayout += count; + } + + @Override + public void offsetPositionsForRemovingLaidOutOrNewView( + int positionStart, int itemCount) { + offsetPositionRecordsForRemove(positionStart, itemCount, false); + mItemsAddedOrRemoved = true; + } + + @Override + public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) { + viewRangeUpdate(positionStart, itemCount, payload); + mItemsChanged = true; + } + + @Override + public void onDispatchFirstPass(AdapterHelper.UpdateOp op) { + dispatchUpdate(op); + } + + void dispatchUpdate(AdapterHelper.UpdateOp op) { + switch (op.cmd) { + case AdapterHelper.UpdateOp.ADD: + mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount); + break; + case AdapterHelper.UpdateOp.REMOVE: + mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount); + break; + case AdapterHelper.UpdateOp.UPDATE: + mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount, + op.payload); + break; + case AdapterHelper.UpdateOp.MOVE: + mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1); + break; + } + } + + @Override + public void onDispatchSecondPass(AdapterHelper.UpdateOp op) { + dispatchUpdate(op); + } + + @Override + public void offsetPositionsForAdd(int positionStart, int itemCount) { + offsetPositionRecordsForInsert(positionStart, itemCount); + mItemsAddedOrRemoved = true; + } + + @Override + public void offsetPositionsForMove(int from, int to) { + offsetPositionRecordsForMove(from, to); + // should we create mItemsMoved ? + mItemsAddedOrRemoved = true; + } + }); + } + + /** + * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's + * size is not affected by the adapter contents. RecyclerView can still change its size based + * on other factors (e.g. its parent's size) but this size calculation cannot depend on the + * size of its children or contents of its adapter (except the number of items in the adapter). + * <p> + * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow + * RecyclerView to avoid invalidating the whole layout when its adapter contents change. + * + * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView. + */ + public void setHasFixedSize(boolean hasFixedSize) { + mHasFixedSize = hasFixedSize; + } + + /** + * @return true if the app has specified that changes in adapter content cannot change + * the size of the RecyclerView itself. + */ + public boolean hasFixedSize() { + return mHasFixedSize; + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + if (clipToPadding != mClipToPadding) { + invalidateGlows(); + } + mClipToPadding = clipToPadding; + super.setClipToPadding(clipToPadding); + if (mFirstLayoutComplete) { + requestLayout(); + } + } + + /** + * Returns whether this RecyclerView will clip its children to its padding, and resize (but + * not clip) any EdgeEffect to the padded region, if padding is present. + * <p> + * By default, children are clipped to the padding of their parent + * RecyclerView. This clipping behavior is only enabled if padding is non-zero. + * + * @return true if this RecyclerView clips children to its padding and resizes (but doesn't + * clip) any EdgeEffect to the padded region, false otherwise. + * + * @attr name android:clipToPadding + */ + @Override + public boolean getClipToPadding() { + return mClipToPadding; + } + + /** + * Configure the scrolling touch slop for a specific use case. + * + * Set up the RecyclerView's scrolling motion threshold based on common usages. + * Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}. + * + * @param slopConstant One of the <code>TOUCH_SLOP_</code> constants representing + * the intended usage of this RecyclerView + */ + public void setScrollingTouchSlop(int slopConstant) { + final ViewConfiguration vc = ViewConfiguration.get(getContext()); + switch (slopConstant) { + default: + Log.w(TAG, "setScrollingTouchSlop(): bad argument constant " + + slopConstant + "; using default value"); + // fall-through + case TOUCH_SLOP_DEFAULT: + mTouchSlop = vc.getScaledTouchSlop(); + break; + + case TOUCH_SLOP_PAGING: + mTouchSlop = vc.getScaledPagingTouchSlop(); + break; + } + } + + /** + * Swaps the current adapter with the provided one. It is similar to + * {@link #setAdapter(Adapter)} but assumes existing adapter and the new adapter uses the same + * {@link ViewHolder} and does not clear the RecycledViewPool. + * <p> + * Note that it still calls onAdapterChanged callbacks. + * + * @param adapter The new adapter to set, or null to set no adapter. + * @param removeAndRecycleExistingViews If set to true, RecyclerView will recycle all existing + * Views. If adapters have stable ids and/or you want to + * animate the disappearing views, you may prefer to set + * this to false. + * @see #setAdapter(Adapter) + */ + public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, true, removeAndRecycleExistingViews); + setDataSetChangedAfterLayout(); + requestLayout(); + } + /** + * Set a new adapter to provide child views on demand. + * <p> + * When adapter is changed, all existing views are recycled back to the pool. If the pool has + * only one adapter, it will be cleared. + * + * @param adapter The new adapter to set, or null to set no adapter. + * @see #swapAdapter(Adapter, boolean) + */ + public void setAdapter(Adapter adapter) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, false, true); + requestLayout(); + } + + /** + * Removes and recycles all views - both those currently attached, and those in the Recycler. + */ + void removeAndRecycleViews() { + // end all running animations + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + // Since animations are ended, mLayout.children should be equal to + // recyclerView.children. This may not be true if item animator's end does not work as + // expected. (e.g. not release children instantly). It is safer to use mLayout's child + // count. + if (mLayout != null) { + mLayout.removeAndRecycleAllViews(mRecycler); + mLayout.removeAndRecycleScrapInt(mRecycler); + } + // we should clear it here before adapters are swapped to ensure correct callbacks. + mRecycler.clear(); + } + + /** + * Replaces the current adapter with the new one and triggers listeners. + * @param adapter The new adapter + * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and + * item types with the current adapter (helps us avoid cache + * invalidation). + * @param removeAndRecycleViews If true, we'll remove and recycle all existing views. If + * compatibleWithPrevious is false, this parameter is ignored. + */ + private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious, + boolean removeAndRecycleViews) { + if (mAdapter != null) { + mAdapter.unregisterAdapterDataObserver(mObserver); + mAdapter.onDetachedFromRecyclerView(this); + } + if (!compatibleWithPrevious || removeAndRecycleViews) { + removeAndRecycleViews(); + } + mAdapterHelper.reset(); + final Adapter oldAdapter = mAdapter; + mAdapter = adapter; + if (adapter != null) { + adapter.registerAdapterDataObserver(mObserver); + adapter.onAttachedToRecyclerView(this); + } + if (mLayout != null) { + mLayout.onAdapterChanged(oldAdapter, mAdapter); + } + mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious); + mState.mStructureChanged = true; + markKnownViewsInvalid(); + } + + /** + * Retrieves the previously set adapter or null if no adapter is set. + * + * @return The previously set adapter + * @see #setAdapter(Adapter) + */ + public Adapter getAdapter() { + return mAdapter; + } + + /** + * Register a listener that will be notified whenever a child view is recycled. + * + * <p>This listener will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates expensive + * or heavyweight data with item views, this may be a good place to release + * or free those resources.</p> + * + * @param listener Listener to register, or null to clear + */ + public void setRecyclerListener(RecyclerListener listener) { + mRecyclerListener = listener; + } + + /** + * <p>Return the offset of the RecyclerView's text baseline from the its top + * boundary. If the LayoutManager of this RecyclerView does not support baseline alignment, + * this method returns -1.</p> + * + * @return the offset of the baseline within the RecyclerView's bounds or -1 + * if baseline alignment is not supported + */ + @Override + public int getBaseline() { + if (mLayout != null) { + return mLayout.getBaseline(); + } else { + return super.getBaseline(); + } + } + + /** + * Register a listener that will be notified whenever a child view is attached to or detached + * from RecyclerView. + * + * <p>This listener will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates expensive + * or heavyweight data with item views, this may be a good place to release + * or free those resources.</p> + * + * @param listener Listener to register + */ + public void addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + mOnChildAttachStateListeners = new ArrayList<>(); + } + mOnChildAttachStateListeners.add(listener); + } + + /** + * Removes the provided listener from child attached state listeners list. + * + * @param listener Listener to unregister + */ + public void removeOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + return; + } + mOnChildAttachStateListeners.remove(listener); + } + + /** + * Removes all listeners that were added via + * {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}. + */ + public void clearOnChildAttachStateChangeListeners() { + if (mOnChildAttachStateListeners != null) { + mOnChildAttachStateListeners.clear(); + } + } + + /** + * Set the {@link LayoutManager} that this RecyclerView will use. + * + * <p>In contrast to other adapter-backed views such as {@link android.widget.ListView} + * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom + * layout arrangements for child views. These arrangements are controlled by the + * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p> + * + * <p>Several default strategies are provided for common uses such as lists and grids.</p> + * + * @param layout LayoutManager to use + */ + public void setLayoutManager(LayoutManager layout) { + if (layout == mLayout) { + return; + } + stopScroll(); + // TODO We should do this switch a dispatchLayout pass and animate children. There is a good + // chance that LayoutManagers will re-use views. + if (mLayout != null) { + // end all running animations + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + mLayout.removeAndRecycleAllViews(mRecycler); + mLayout.removeAndRecycleScrapInt(mRecycler); + mRecycler.clear(); + + if (mIsAttached) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mLayout.setRecyclerView(null); + mLayout = null; + } else { + mRecycler.clear(); + } + // this is just a defensive measure for faulty item animators. + mChildHelper.removeAllViewsUnfiltered(); + mLayout = layout; + if (layout != null) { + if (layout.mRecyclerView != null) { + throw new IllegalArgumentException("LayoutManager " + layout + + " is already attached to a RecyclerView: " + layout.mRecyclerView); + } + mLayout.setRecyclerView(this); + if (mIsAttached) { + mLayout.dispatchAttachedToWindow(this); + } + } + mRecycler.updateViewCacheSize(); + requestLayout(); + } + + /** + * Set a {@link OnFlingListener} for this {@link RecyclerView}. + * <p> + * If the {@link OnFlingListener} is set then it will receive + * calls to {@link #fling(int,int)} and will be able to intercept them. + * + * @param onFlingListener The {@link OnFlingListener} instance. + */ + public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) { + mOnFlingListener = onFlingListener; + } + + /** + * Get the current {@link OnFlingListener} from this {@link RecyclerView}. + * + * @return The {@link OnFlingListener} instance currently set (can be null). + */ + @Nullable + public OnFlingListener getOnFlingListener() { + return mOnFlingListener; + } + + @Override + protected Parcelable onSaveInstanceState() { + SavedState state = new SavedState(super.onSaveInstanceState()); + if (mPendingSavedState != null) { + state.copyFrom(mPendingSavedState); + } else if (mLayout != null) { + state.mLayoutState = mLayout.onSaveInstanceState(); + } else { + state.mLayoutState = null; + } + + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + mPendingSavedState = (SavedState) state; + super.onRestoreInstanceState(mPendingSavedState.getSuperState()); + if (mLayout != null && mPendingSavedState.mLayoutState != null) { + mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState); + } + } + + /** + * Override to prevent freezing of any views created by the adapter. + */ + @Override + protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any views created by the adapter. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + dispatchThawSelfOnly(container); + } + + /** + * Adds a view to the animatingViews list. + * mAnimatingViews holds the child views that are currently being kept around + * purely for the purpose of being animated out of view. They are drawn as a regular + * part of the child list of the RecyclerView, but they are invisible to the LayoutManager + * as they are managed separately from the regular child views. + * @param viewHolder The ViewHolder to be removed + */ + private void addAnimatingView(ViewHolder viewHolder) { + final View view = viewHolder.itemView; + final boolean alreadyParented = view.getParent() == this; + mRecycler.unscrapView(getChildViewHolder(view)); + if (viewHolder.isTmpDetached()) { + // re-attach + mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true); + } else if (!alreadyParented) { + mChildHelper.addView(view, true); + } else { + mChildHelper.hide(view); + } + } + + /** + * Removes a view from the animatingViews list. + * @param view The view to be removed + * @see #addAnimatingView(RecyclerView.ViewHolder) + * @return true if an animating view is removed + */ + boolean removeAnimatingView(View view) { + eatRequestLayout(); + final boolean removed = mChildHelper.removeViewIfHidden(view); + if (removed) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + mRecycler.unscrapView(viewHolder); + mRecycler.recycleViewHolderInternal(viewHolder); + if (DEBUG) { + Log.d(TAG, "after removing animated view: " + view + ", " + this); + } + } + // only clear request eaten flag if we removed the view. + resumeRequestLayout(!removed); + return removed; + } + + /** + * Return the {@link LayoutManager} currently responsible for + * layout policy for this RecyclerView. + * + * @return The currently bound LayoutManager + */ + public LayoutManager getLayoutManager() { + return mLayout; + } + + /** + * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null; + * if no pool is set for this view a new one will be created. See + * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information. + * + * @return The pool used to store recycled item views for reuse. + * @see #setRecycledViewPool(RecycledViewPool) + */ + public RecycledViewPool getRecycledViewPool() { + return mRecycler.getRecycledViewPool(); + } + + /** + * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. + * This can be useful if you have multiple RecyclerViews with adapters that use the same + * view types, for example if you have several data sets with the same kinds of item views + * displayed by a {@link android.support.v4.view.ViewPager ViewPager}. + * + * @param pool Pool to set. If this parameter is null a new pool will be created and used. + */ + public void setRecycledViewPool(RecycledViewPool pool) { + mRecycler.setRecycledViewPool(pool); + } + + /** + * Sets a new {@link ViewCacheExtension} to be used by the Recycler. + * + * @param extension ViewCacheExtension to be used or null if you want to clear the existing one. + * + * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)} + */ + public void setViewCacheExtension(ViewCacheExtension extension) { + mRecycler.setViewCacheExtension(extension); + } + + /** + * Set the number of offscreen views to retain before adding them to the potentially shared + * {@link #getRecycledViewPool() recycled view pool}. + * + * <p>The offscreen view cache stays aware of changes in the attached adapter, allowing + * a LayoutManager to reuse those views unmodified without needing to return to the adapter + * to rebind them.</p> + * + * @param size Number of views to cache offscreen before returning them to the general + * recycled view pool + */ + public void setItemViewCacheSize(int size) { + mRecycler.setViewCacheSize(size); + } + + /** + * Return the current scrolling state of the RecyclerView. + * + * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or + * {@link #SCROLL_STATE_SETTLING} + */ + public int getScrollState() { + return mScrollState; + } + + void setScrollState(int state) { + if (state == mScrollState) { + return; + } + if (DEBUG) { + Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, + new Exception()); + } + mScrollState = state; + if (state != SCROLL_STATE_SETTLING) { + stopScrollersInternal(); + } + dispatchOnScrollStateChanged(state); + } + + /** + * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can + * affect both measurement and drawing of individual item views. + * + * <p>Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.</p> + * + * @param decor Decoration to add + * @param index Position in the decoration chain to insert this decoration at. If this value + * is negative the decoration will be added at the end. + */ + public void addItemDecoration(ItemDecoration decor, int index) { + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or" + + " layout"); + } + if (mItemDecorations.isEmpty()) { + setWillNotDraw(false); + } + if (index < 0) { + mItemDecorations.add(decor); + } else { + mItemDecorations.add(index, decor); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can + * affect both measurement and drawing of individual item views. + * + * <p>Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.</p> + * + * @param decor Decoration to add + */ + public void addItemDecoration(ItemDecoration decor) { + addItemDecoration(decor, -1); + } + + /** + * Remove an {@link ItemDecoration} from this RecyclerView. + * + * <p>The given decoration will no longer impact the measurement and drawing of + * item views.</p> + * + * @param decor Decoration to remove + * @see #addItemDecoration(ItemDecoration) + */ + public void removeItemDecoration(ItemDecoration decor) { + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll or" + + " layout"); + } + mItemDecorations.remove(decor); + if (mItemDecorations.isEmpty()) { + setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Sets the {@link ChildDrawingOrderCallback} to be used for drawing children. + * <p> + * See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will + * always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be + * true if childDrawingOrderCallback is not null, false otherwise. + * <p> + * Note that child drawing order may be overridden by View's elevation. + * + * @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing + * system. + */ + public void setChildDrawingOrderCallback(ChildDrawingOrderCallback childDrawingOrderCallback) { + if (childDrawingOrderCallback == mChildDrawingOrderCallback) { + return; + } + mChildDrawingOrderCallback = childDrawingOrderCallback; + setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null); + } + + /** + * Set a listener that will be notified of any changes in scroll state or position. + * + * @param listener Listener to set or null to clear + * + * @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and + * {@link #removeOnScrollListener(OnScrollListener)} + */ + @Deprecated + public void setOnScrollListener(OnScrollListener listener) { + mScrollListener = listener; + } + + /** + * Add a listener that will be notified of any changes in scroll state or position. + * + * <p>Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnScrollListeners()} + * to remove all attached listeners.</p> + * + * @param listener listener to set or null to clear + */ + public void addOnScrollListener(OnScrollListener listener) { + if (mScrollListeners == null) { + mScrollListeners = new ArrayList<>(); + } + mScrollListeners.add(listener); + } + + /** + * Remove a listener that was notified of any changes in scroll state or position. + * + * @param listener listener to set or null to clear + */ + public void removeOnScrollListener(OnScrollListener listener) { + if (mScrollListeners != null) { + mScrollListeners.remove(listener); + } + } + + /** + * Remove all secondary listener that were notified of any changes in scroll state or position. + */ + public void clearOnScrollListeners() { + if (mScrollListeners != null) { + mScrollListeners.clear(); + } + } + + /** + * Convenience method to scroll to a certain position. + * + * RecyclerView does not implement scrolling logic, rather forwards the call to + * {@link com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)} + * @param position Scroll to this adapter position + * @see com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int) + */ + public void scrollToPosition(int position) { + if (mLayoutFrozen) { + return; + } + stopScroll(); + if (mLayout == null) { + Log.e(TAG, "Cannot scroll to position a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + mLayout.scrollToPosition(position); + awakenScrollBars(); + } + + void jumpToPositionForSmoothScroller(int position) { + if (mLayout == null) { + return; + } + mLayout.scrollToPosition(position); + awakenScrollBars(); + } + + /** + * Starts a smooth scroll to an adapter position. + * <p> + * To support smooth scrolling, you must override + * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a + * {@link SmoothScroller}. + * <p> + * {@link LayoutManager} is responsible for creating the actual scroll action. If you want to + * provide a custom smooth scroll logic, override + * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your + * LayoutManager. + * + * @param position The adapter position to scroll to + * @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int) + */ + public void smoothScrollToPosition(int position) { + if (mLayoutFrozen) { + return; + } + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + mLayout.smoothScrollToPosition(this, mState, position); + } + + @Override + public void scrollTo(int x, int y) { + Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. " + + "Use scrollToPosition instead"); + } + + @Override + public void scrollBy(int x, int y) { + if (mLayout == null) { + Log.e(TAG, "Cannot scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutFrozen) { + return; + } + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + if (canScrollHorizontal || canScrollVertical) { + scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null); + } + } + + /** + * Helper method reflect data changes to the state. + * <p> + * Adapter changes during a scroll may trigger a crash because scroll assumes no data change + * but data actually changed. + * <p> + * This method consumes all deferred changes to avoid that case. + */ + void consumePendingUpdateOperations() { + if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) { + Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + Trace.endSection(); + return; + } + if (!mAdapterHelper.hasPendingUpdates()) { + return; + } + + // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any + // of the visible items is affected and if not, just ignore the change. + if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper + .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE + | AdapterHelper.UpdateOp.MOVE)) { + Trace.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); + eatRequestLayout(); + onEnterLayoutOrScroll(); + mAdapterHelper.preProcess(); + if (!mLayoutRequestEaten) { + if (hasUpdatedView()) { + dispatchLayout(); + } else { + // no need to layout, clean state + mAdapterHelper.consumePostponedUpdates(); + } + } + resumeRequestLayout(true); + onExitLayoutOrScroll(); + Trace.endSection(); + } else if (mAdapterHelper.hasPendingUpdates()) { + Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + Trace.endSection(); + } + } + + /** + * @return True if an existing view holder needs to be updated + */ + private boolean hasUpdatedView() { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder == null || holder.shouldIgnore()) { + continue; + } + if (holder.isUpdated()) { + return true; + } + } + return false; + } + + /** + * Does not perform bounds checking. Used by internal methods that have already validated input. + * <p> + * It also reports any unused scroll request to the related EdgeEffect. + * + * @param x The amount of horizontal scroll request + * @param y The amount of vertical scroll request + * @param ev The originating MotionEvent, or null if not from a touch event. + * + * @return Whether any scroll was consumed in either direction. + */ + boolean scrollByInternal(int x, int y, MotionEvent ev) { + int unconsumedX = 0, unconsumedY = 0; + int consumedX = 0, consumedY = 0; + + consumePendingUpdateOperations(); + if (mAdapter != null) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + Trace.beginSection(TRACE_SCROLL_TAG); + if (x != 0) { + consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); + unconsumedX = x - consumedX; + } + if (y != 0) { + consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); + unconsumedY = y - consumedY; + } + Trace.endSection(); + repositionShadowingViews(); + onExitLayoutOrScroll(); + resumeRequestLayout(false); + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + + if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) { + // Update the last touch co-ords, taking any scroll offset into account + mLastTouchX -= mScrollOffset[0]; + mLastTouchY -= mScrollOffset[1]; + if (ev != null) { + ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + } + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (ev != null) { + pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); + } + considerReleasingGlowsOnScroll(x, y); + } + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); + } + if (!awakenScrollBars()) { + invalidate(); + } + return consumedX != 0 || consumedY != 0; + } + + /** + * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal + * range. This value is used to compute the length of the thumb within the scrollbar's track. + * </p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your + * LayoutManager. </p> + * + * @return The horizontal offset of the scrollbar's thumb + * @see com.android.internal.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset + * (RecyclerView.State) + */ + @Override + public int computeHorizontalScrollOffset() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0; + } + + /** + * <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the + * horizontal range. This value is used to compute the length of the thumb within the + * scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your + * LayoutManager.</p> + * + * @return The horizontal extent of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State) + */ + @Override + public int computeHorizontalScrollExtent() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0; + } + + /** + * <p>Compute the horizontal range that the horizontal scrollbar represents.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your + * LayoutManager.</p> + * + * @return The total horizontal range represented by the vertical scrollbar + * @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State) + */ + @Override + public int computeHorizontalScrollRange() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0; + } + + /** + * <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range. + * This value is used to compute the length of the thumb within the scrollbar's track. </p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your + * LayoutManager.</p> + * + * @return The vertical offset of the scrollbar's thumb + * @see com.android.internal.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset + * (RecyclerView.State) + */ + @Override + public int computeVerticalScrollOffset() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0; + } + + /** + * <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range. + * This value is used to compute the length of the thumb within the scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your + * LayoutManager.</p> + * + * @return The vertical extent of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State) + */ + @Override + public int computeVerticalScrollExtent() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0; + } + + /** + * <p>Compute the vertical range that the vertical scrollbar represents.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p> + * + * <p>Default implementation returns 0.</p> + * + * <p>If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your + * LayoutManager.</p> + * + * @return The total vertical range represented by the vertical scrollbar + * @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State) + */ + @Override + public int computeVerticalScrollRange() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0; + } + + + void eatRequestLayout() { + mEatRequestLayout++; + if (mEatRequestLayout == 1 && !mLayoutFrozen) { + mLayoutRequestEaten = false; + } + } + + void resumeRequestLayout(boolean performLayoutChildren) { + if (mEatRequestLayout < 1) { + //noinspection PointlessBooleanExpression + if (DEBUG) { + throw new IllegalStateException("invalid eat request layout count"); + } + mEatRequestLayout = 1; + } + if (!performLayoutChildren) { + // Reset the layout request eaten counter. + // This is necessary since eatRequest calls can be nested in which case the other + // call will override the inner one. + // for instance: + // eat layout for process adapter updates + // eat layout for dispatchLayout + // a bunch of req layout calls arrive + + mLayoutRequestEaten = false; + } + if (mEatRequestLayout == 1) { + // when layout is frozen we should delay dispatchLayout() + if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen + && mLayout != null && mAdapter != null) { + dispatchLayout(); + } + if (!mLayoutFrozen) { + mLayoutRequestEaten = false; + } + } + mEatRequestLayout--; + } + + /** + * Enable or disable layout and scroll. After <code>setLayoutFrozen(true)</code> is called, + * Layout requests will be postponed until <code>setLayoutFrozen(false)</code> is called; + * child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)}, + * {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and + * {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are + * dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be + * called. + * + * <p> + * <code>setLayoutFrozen(true)</code> does not prevent app from directly calling {@link + * LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition( + * RecyclerView, State, int)}. + * <p> + * {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically + * stop frozen. + * <p> + * Note: Running ItemAnimator is not stopped automatically, it's caller's + * responsibility to call ItemAnimator.end(). + * + * @param frozen true to freeze layout and scroll, false to re-enable. + */ + public void setLayoutFrozen(boolean frozen) { + if (frozen != mLayoutFrozen) { + assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll"); + if (!frozen) { + mLayoutFrozen = false; + if (mLayoutRequestEaten && mLayout != null && mAdapter != null) { + requestLayout(); + } + mLayoutRequestEaten = false; + } else { + final long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + onTouchEvent(cancelEvent); + mLayoutFrozen = true; + mIgnoreMotionEventTillDown = true; + stopScroll(); + } + } + } + + /** + * Returns true if layout and scroll are frozen. + * + * @return true if layout and scroll are frozen + * @see #setLayoutFrozen(boolean) + */ + public boolean isLayoutFrozen() { + return mLayoutFrozen; + } + + /** + * Animate a scroll by the given amount of pixels along either axis. + * + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + */ + public void smoothScrollBy(int dx, int dy) { + smoothScrollBy(dx, dy, null); + } + + /** + * Animate a scroll by the given amount of pixels along either axis. + * + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param interpolator {@link Interpolator} to be used for scrolling. If it is + * {@code null}, RecyclerView is going to use the default interpolator. + */ + public void smoothScrollBy(int dx, int dy, Interpolator interpolator) { + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutFrozen) { + return; + } + if (!mLayout.canScrollHorizontally()) { + dx = 0; + } + if (!mLayout.canScrollVertically()) { + dy = 0; + } + if (dx != 0 || dy != 0) { + mViewFlinger.smoothScrollBy(dx, dy, interpolator); + } + } + + /** + * Begin a standard fling with an initial velocity along each axis in pixels per second. + * If the velocity given is below the system-defined minimum this method will return false + * and no fling will occur. + * + * @param velocityX Initial horizontal velocity in pixels per second + * @param velocityY Initial vertical velocity in pixels per second + * @return true if the fling was started, false if the velocity was too low to fling or + * LayoutManager does not support scrolling in the axis fling is issued. + * + * @see LayoutManager#canScrollVertically() + * @see LayoutManager#canScrollHorizontally() + */ + public boolean fling(int velocityX, int velocityY) { + if (mLayout == null) { + Log.e(TAG, "Cannot fling without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return false; + } + if (mLayoutFrozen) { + return false; + } + + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + + if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { + velocityX = 0; + } + if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { + velocityY = 0; + } + if (velocityX == 0 && velocityY == 0) { + // If we don't have any velocity, return false + return false; + } + + if (!dispatchNestedPreFling(velocityX, velocityY)) { + final boolean canScroll = canScrollHorizontal || canScrollVertical; + dispatchNestedFling(velocityX, velocityY, canScroll); + + if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { + return true; + } + + if (canScroll) { + velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); + velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); + mViewFlinger.fling(velocityX, velocityY); + return true; + } + } + return false; + } + + /** + * Stop any current scroll in progress, such as one started by + * {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling. + */ + public void stopScroll() { + setScrollState(SCROLL_STATE_IDLE); + stopScrollersInternal(); + } + + /** + * Similar to {@link #stopScroll()} but does not set the state. + */ + private void stopScrollersInternal() { + mViewFlinger.stop(); + if (mLayout != null) { + mLayout.stopSmoothScroller(); + } + } + + /** + * Returns the minimum velocity to start a fling. + * + * @return The minimum velocity to start a fling + */ + public int getMinFlingVelocity() { + return mMinFlingVelocity; + } + + + /** + * Returns the maximum fling velocity used by this RecyclerView. + * + * @return The maximum fling velocity used by this RecyclerView. + */ + public int getMaxFlingVelocity() { + return mMaxFlingVelocity; + } + + /** + * Apply a pull to relevant overscroll glow effects + */ + private void pullGlows(float x, float overscrollX, float y, float overscrollY) { + boolean invalidate = false; + if (overscrollX < 0) { + ensureLeftGlow(); + mLeftGlow.onPull(-overscrollX / getWidth(), 1f - y / getHeight()); + invalidate = true; + } else if (overscrollX > 0) { + ensureRightGlow(); + mRightGlow.onPull(overscrollX / getWidth(), y / getHeight()); + invalidate = true; + } + + if (overscrollY < 0) { + ensureTopGlow(); + mTopGlow.onPull(-overscrollY / getHeight(), x / getWidth()); + invalidate = true; + } else if (overscrollY > 0) { + ensureBottomGlow(); + mBottomGlow.onPull(overscrollY / getHeight(), 1f - x / getWidth()); + invalidate = true; + } + + if (invalidate || overscrollX != 0 || overscrollY != 0) { + postInvalidateOnAnimation(); + } + } + + private void releaseGlows() { + boolean needsInvalidate = false; + if (mLeftGlow != null) { + mLeftGlow.onRelease(); + needsInvalidate = true; + } + if (mTopGlow != null) { + mTopGlow.onRelease(); + needsInvalidate = true; + } + if (mRightGlow != null) { + mRightGlow.onRelease(); + needsInvalidate = true; + } + if (mBottomGlow != null) { + mBottomGlow.onRelease(); + needsInvalidate = true; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + } + + void considerReleasingGlowsOnScroll(int dx, int dy) { + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) { + mLeftGlow.onRelease(); + needsInvalidate = true; + } + if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) { + mRightGlow.onRelease(); + needsInvalidate = true; + } + if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) { + mTopGlow.onRelease(); + needsInvalidate = true; + } + if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) { + mBottomGlow.onRelease(); + needsInvalidate = true; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + } + + void absorbGlows(int velocityX, int velocityY) { + if (velocityX < 0) { + ensureLeftGlow(); + mLeftGlow.onAbsorb(-velocityX); + } else if (velocityX > 0) { + ensureRightGlow(); + mRightGlow.onAbsorb(velocityX); + } + + if (velocityY < 0) { + ensureTopGlow(); + mTopGlow.onAbsorb(-velocityY); + } else if (velocityY > 0) { + ensureBottomGlow(); + mBottomGlow.onAbsorb(velocityY); + } + + if (velocityX != 0 || velocityY != 0) { + postInvalidateOnAnimation(); + } + } + + void ensureLeftGlow() { + if (mLeftGlow != null) { + return; + } + mLeftGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureRightGlow() { + if (mRightGlow != null) { + return; + } + mRightGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureTopGlow() { + if (mTopGlow != null) { + return; + } + mTopGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + + } + + void ensureBottomGlow() { + if (mBottomGlow != null) { + return; + } + mBottomGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + } + + void invalidateGlows() { + mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null; + } + + /** + * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are + * in the Adapter but not visible in the UI), it employs a more involved focus search strategy + * that differs from other ViewGroups. + * <p> + * It first does a focus search within the RecyclerView. If this search finds a View that is in + * the focus direction with respect to the currently focused View, RecyclerView returns that + * child as the next focus target. When it cannot find such child, it calls + * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} to layout more Views + * in the focus search direction. If LayoutManager adds a View that matches the + * focus search criteria, it will be returned as the focus search result. Otherwise, + * RecyclerView will call parent to handle the focus search like a regular ViewGroup. + * <p> + * When the direction is {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}, a View that + * is not in the focus direction is still valid focus target which may not be the desired + * behavior if the Adapter has more children in the focus direction. To handle this case, + * RecyclerView converts the focus direction to an absolute direction and makes a preliminary + * focus search in that direction. If there are no Views to gain focus, it will call + * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} before running a + * focus search with the original (relative) direction. This allows RecyclerView to provide + * better candidates to the focus search while still allowing the view system to take focus from + * the RecyclerView and give it to a more suitable child if such child exists. + * + * @param focused The view that currently has focus + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD} or 0 for not applicable. + * + * @return A new View that can be the next focus after the focused View + */ + @Override + public View focusSearch(View focused, int direction) { + View result = mLayout.onInterceptFocusSearch(focused, direction); + if (result != null) { + return result; + } + final boolean canRunFocusFailure = mAdapter != null && mLayout != null + && !isComputingLayout() && !mLayoutFrozen; + + final FocusFinder ff = FocusFinder.getInstance(); + if (canRunFocusFailure + && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) { + // convert direction to absolute direction and see if we have a view there and if not + // tell LayoutManager to add if it can. + boolean needsFocusFailureLayout = false; + if (mLayout.canScrollVertically()) { + final int absDir = + direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP; + final View found = ff.findNextFocus(this, focused, absDir); + needsFocusFailureLayout = found == null; + if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { + // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. + direction = absDir; + } + } + if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) { + boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl + ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + final View found = ff.findNextFocus(this, focused, absDir); + needsFocusFailureLayout = found == null; + if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { + // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. + direction = absDir; + } + } + if (needsFocusFailureLayout) { + consumePendingUpdateOperations(); + final View focusedItemView = findContainingItemView(focused); + if (focusedItemView == null) { + // panic, focused view is not a child anymore, cannot call super. + return null; + } + eatRequestLayout(); + mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + resumeRequestLayout(false); + } + result = ff.findNextFocus(this, focused, direction); + } else { + result = ff.findNextFocus(this, focused, direction); + if (result == null && canRunFocusFailure) { + consumePendingUpdateOperations(); + final View focusedItemView = findContainingItemView(focused); + if (focusedItemView == null) { + // panic, focused view is not a child anymore, cannot call super. + return null; + } + eatRequestLayout(); + result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + resumeRequestLayout(false); + } + } + return isPreferredNextFocus(focused, result, direction) + ? result : super.focusSearch(focused, direction); + } + + /** + * Checks if the new focus candidate is a good enough candidate such that RecyclerView will + * assign it as the next focus View instead of letting view hierarchy decide. + * A good candidate means a View that is aligned in the focus direction wrt the focused View + * and is not the RecyclerView itself. + * When this method returns false, RecyclerView will let the parent make the decision so the + * same View may still get the focus as a result of that search. + */ + private boolean isPreferredNextFocus(View focused, View next, int direction) { + if (next == null || next == this) { + return false; + } + if (focused == null) { + return true; + } + + if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { + final boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final int absHorizontal = (direction == View.FOCUS_FORWARD) ^ rtl + ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + if (isPreferredNextFocusAbsolute(focused, next, absHorizontal)) { + return true; + } + if (direction == View.FOCUS_FORWARD) { + return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_DOWN); + } else { + return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_UP); + } + } else { + return isPreferredNextFocusAbsolute(focused, next, direction); + } + + } + + /** + * Logic taken from FocusSearch#isCandidate + */ + private boolean isPreferredNextFocusAbsolute(View focused, View next, int direction) { + mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); + mTempRect2.set(0, 0, next.getWidth(), next.getHeight()); + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetDescendantRectToMyCoords(next, mTempRect2); + switch (direction) { + case View.FOCUS_LEFT: + return (mTempRect.right > mTempRect2.right + || mTempRect.left >= mTempRect2.right) + && mTempRect.left > mTempRect2.left; + case View.FOCUS_RIGHT: + return (mTempRect.left < mTempRect2.left + || mTempRect.right <= mTempRect2.left) + && mTempRect.right < mTempRect2.right; + case View.FOCUS_UP: + return (mTempRect.bottom > mTempRect2.bottom + || mTempRect.top >= mTempRect2.bottom) + && mTempRect.top > mTempRect2.top; + case View.FOCUS_DOWN: + return (mTempRect.top < mTempRect2.top + || mTempRect.bottom <= mTempRect2.top) + && mTempRect.bottom < mTempRect2.bottom; + } + throw new IllegalArgumentException("direction must be absolute. received:" + direction); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) { + mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); + + // get item decor offsets w/o refreshing. If they are invalid, there will be another + // layout pass to fix them, then it is LayoutManager's responsibility to keep focused + // View in viewport. + final ViewGroup.LayoutParams focusedLayoutParams = focused.getLayoutParams(); + if (focusedLayoutParams instanceof LayoutParams) { + // if focused child has item decors, use them. Otherwise, ignore. + final LayoutParams lp = (LayoutParams) focusedLayoutParams; + if (!lp.mInsetsDirty) { + final Rect insets = lp.mDecorInsets; + mTempRect.left -= insets.left; + mTempRect.right += insets.right; + mTempRect.top -= insets.top; + mTempRect.bottom += insets.bottom; + } + } + + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetRectIntoDescendantCoords(child, mTempRect); + requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete); + } + super.requestChildFocus(child, focused); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate); + } + + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) { + super.addFocusables(views, direction, focusableMode); + } + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + if (isComputingLayout()) { + // if we are in the middle of a layout calculation, don't let any child take focus. + // RV will handle it after layout calculation is finished. + return false; + } + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mLayoutOrScrollCounter = 0; + mIsAttached = true; + mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested(); + if (mLayout != null) { + mLayout.dispatchAttachedToWindow(this); + } + mPostedAnimatorRunner = false; + + if (ALLOW_THREAD_GAP_WORK) { + // Register with gap worker + mGapWorker = GapWorker.sGapWorker.get(); + if (mGapWorker == null) { + mGapWorker = new GapWorker(); + + // break 60 fps assumption if data from display appears valid + // NOTE: we only do this query once, statically, because it's very expensive (> 1ms) + Display display = getDisplay(); + float refreshRate = 60.0f; + if (!isInEditMode() && display != null) { + float displayRefreshRate = display.getRefreshRate(); + if (displayRefreshRate >= 30.0f) { + refreshRate = displayRefreshRate; + } + } + mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate); + GapWorker.sGapWorker.set(mGapWorker); + } + mGapWorker.add(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + stopScroll(); + mIsAttached = false; + if (mLayout != null) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mPendingAccessibilityImportanceChange.clear(); + removeCallbacks(mItemAnimatorRunner); + mViewInfoStore.onDetach(); + + if (ALLOW_THREAD_GAP_WORK) { + // Unregister with gap worker + mGapWorker.remove(this); + mGapWorker = null; + } + } + + /** + * Returns true if RecyclerView is attached to window. + */ + // @override + public boolean isAttachedToWindow() { + return mIsAttached; + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it <b>is not</b>. + * + * @param message The message for the exception. Can be null. + * @see #assertNotInLayoutOrScroll(String) + */ + void assertInLayoutOrScroll(String message) { + if (!isComputingLayout()) { + if (message == null) { + throw new IllegalStateException("Cannot call this method unless RecyclerView is " + + "computing a layout or scrolling"); + } + throw new IllegalStateException(message); + + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it <b>is</b>. + * + * @param message The message for the exception. Can be null. + * @see #assertInLayoutOrScroll(String) + */ + void assertNotInLayoutOrScroll(String message) { + if (isComputingLayout()) { + if (message == null) { + throw new IllegalStateException("Cannot call this method while RecyclerView is " + + "computing a layout or scrolling"); + } + throw new IllegalStateException(message); + } + if (mDispatchScrollCounter > 0) { + Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might be run" + + " during a measure & layout pass where you cannot change the RecyclerView" + + " data. Any method call that might change the structure of the RecyclerView" + + " or the adapter contents should be postponed to the next frame.", + new IllegalStateException("")); + } + } + + /** + * Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched + * to child views or this view's standard scrolling behavior. + * + * <p>Client code may use listeners to implement item manipulation behavior. Once a listener + * returns true from + * {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its + * {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called + * for each incoming MotionEvent until the end of the gesture.</p> + * + * @param listener Listener to add + * @see SimpleOnItemTouchListener + */ + public void addOnItemTouchListener(OnItemTouchListener listener) { + mOnItemTouchListeners.add(listener); + } + + /** + * Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events. + * + * @param listener Listener to remove + */ + public void removeOnItemTouchListener(OnItemTouchListener listener) { + mOnItemTouchListeners.remove(listener); + if (mActiveOnItemTouchListener == listener) { + mActiveOnItemTouchListener = null; + } + } + + private boolean dispatchOnItemTouchIntercept(MotionEvent e) { + final int action = e.getAction(); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) { + mActiveOnItemTouchListener = null; + } + + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) { + mActiveOnItemTouchListener = listener; + return true; + } + } + return false; + } + + private boolean dispatchOnItemTouch(MotionEvent e) { + final int action = e.getAction(); + if (mActiveOnItemTouchListener != null) { + if (action == MotionEvent.ACTION_DOWN) { + // Stale state from a previous gesture, we're starting a new one. Clear it. + mActiveOnItemTouchListener = null; + } else { + mActiveOnItemTouchListener.onTouchEvent(this, e); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Clean up for the next gesture. + mActiveOnItemTouchListener = null; + } + return true; + } + } + + // Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept + // as called from onInterceptTouchEvent; skip it. + if (action != MotionEvent.ACTION_DOWN) { + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + if (listener.onInterceptTouchEvent(this, e)) { + mActiveOnItemTouchListener = listener; + return true; + } + } + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + if (mLayoutFrozen) { + // When layout is frozen, RV does not intercept the motion event. + // A child view e.g. a button may still get the click. + return false; + } + if (dispatchOnItemTouchIntercept(e)) { + cancelTouch(); + return true; + } + + if (mLayout == null) { + return false; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(e); + + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mIgnoreMotionEventTillDown) { + mIgnoreMotionEventTillDown = false; + } + mScrollPointerId = e.getPointerId(0); + mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + + if (mScrollState == SCROLL_STATE_SETTLING) { + getParent().requestDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } + + // Clear the nested offsets + mNestedOffsets[0] = mNestedOffsets[1] = 0; + + int nestedScrollAxis = View.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + mScrollPointerId = e.getPointerId(actionIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f); + break; + + case MotionEvent.ACTION_MOVE: { + final int index = e.findPointerIndex(mScrollPointerId); + if (index < 0) { + Log.e(TAG, "Error processing scroll; pointer index for id " + + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); + return false; + } + + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); + if (mScrollState != SCROLL_STATE_DRAGGING) { + final int dx = x - mInitialTouchX; + final int dy = y - mInitialTouchY; + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1); + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1); + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + } break; + + case MotionEvent.ACTION_POINTER_UP: { + onPointerUp(e); + } break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.clear(); + stopNestedScroll(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } + } + return mScrollState == SCROLL_STATE_DRAGGING; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + listener.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + if (mLayoutFrozen || mIgnoreMotionEventTillDown) { + return false; + } + if (dispatchOnItemTouch(e)) { + cancelTouch(); + return true; + } + + if (mLayout == null) { + return false; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + boolean eventAddedToVelocityTracker = false; + + final MotionEvent vtev = MotionEvent.obtain(e); + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); + + if (action == MotionEvent.ACTION_DOWN) { + mNestedOffsets[0] = mNestedOffsets[1] = 0; + } + vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mScrollPointerId = e.getPointerId(0); + mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + + int nestedScrollAxis = View.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); + } break; + + case MotionEvent.ACTION_POINTER_DOWN: { + mScrollPointerId = e.getPointerId(actionIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f); + } break; + + case MotionEvent.ACTION_MOVE: { + final int index = e.findPointerIndex(mScrollPointerId); + if (index < 0) { + Log.e(TAG, "Error processing scroll; pointer index for id " + + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); + return false; + } + + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); + int dx = mLastTouchX - x; + int dy = mLastTouchY - y; + + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + // Updated the nested offsets + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } + + if (mScrollState != SCROLL_STATE_DRAGGING) { + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + if (dx > 0) { + dx -= mTouchSlop; + } else { + dx += mTouchSlop; + } + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop; + } else { + dy += mTouchSlop; + } + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + + if (mScrollState == SCROLL_STATE_DRAGGING) { + mLastTouchX = x - mScrollOffset[0]; + mLastTouchY = y - mScrollOffset[1]; + + if (scrollByInternal( + canScrollHorizontally ? dx : 0, + canScrollVertically ? dy : 0, + vtev)) { + getParent().requestDisallowInterceptTouchEvent(true); + } + if (mGapWorker != null && (dx != 0 || dy != 0)) { + mGapWorker.postFromTraversal(this, dx, dy); + } + } + } break; + + case MotionEvent.ACTION_POINTER_UP: { + onPointerUp(e); + } break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.addMovement(vtev); + eventAddedToVelocityTracker = true; + mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); + final float xvel = canScrollHorizontally + ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; + final float yvel = canScrollVertically + ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; + if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { + setScrollState(SCROLL_STATE_IDLE); + } + resetTouch(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } break; + } + + if (!eventAddedToVelocityTracker) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + + return true; + } + + private void resetTouch() { + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + stopNestedScroll(); + releaseGlows(); + } + + private void cancelTouch() { + resetTouch(); + setScrollState(SCROLL_STATE_IDLE); + } + + private void onPointerUp(MotionEvent e) { + final int actionIndex = e.getActionIndex(); + if (e.getPointerId(actionIndex) == mScrollPointerId) { + // Pick a new pointer to pick up the slack. + final int newIndex = actionIndex == 0 ? 1 : 0; + mScrollPointerId = e.getPointerId(newIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f); + } + } + + // @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if (mLayout == null) { + return false; + } + if (mLayoutFrozen) { + return false; + } + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + if (event.getAction() == MotionEvent.ACTION_SCROLL) { + final float vScroll, hScroll; + if (mLayout.canScrollVertically()) { + // Inverse the sign of the vertical scroll to align the scroll orientation + // with AbsListView. + vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vScroll = 0f; + } + if (mLayout.canScrollHorizontally()) { + hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } else { + hScroll = 0f; + } + + if (vScroll != 0 || hScroll != 0) { + final float scrollFactor = getScrollFactor(); + scrollByInternal((int) (hScroll * scrollFactor), + (int) (vScroll * scrollFactor), event); + } + } + } + return false; + } + + /** + * Ported from View.getVerticalScrollFactor. + */ + private float getScrollFactor() { + if (mScrollFactor == Float.MIN_VALUE) { + TypedValue outValue = new TypedValue(); + if (getContext().getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + mScrollFactor = outValue.getDimension( + getContext().getResources().getDisplayMetrics()); + } else { + return 0; //listPreferredItemHeight is not defined, no generic scrolling + } + } + return mScrollFactor; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (mLayout == null) { + defaultOnMeasure(widthSpec, heightSpec); + return; + } + if (mLayout.mAutoMeasure) { + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); + final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY + && heightMode == MeasureSpec.EXACTLY; + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + if (skipMeasure || mAdapter == null) { + return; + } + if (mState.mLayoutStep == State.STEP_START) { + dispatchLayoutStep1(); + } + // set dimensions in 2nd step. Pre-layout should happen with old dimensions for + // consistency + mLayout.setMeasureSpecs(widthSpec, heightSpec); + mState.mIsMeasuring = true; + dispatchLayoutStep2(); + + // now we can get the width and height from the children. + mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); + + // if RecyclerView has non-exact width and height and if there is at least one child + // which also has non-exact width & height, we have to re-measure. + if (mLayout.shouldMeasureTwice()) { + mLayout.setMeasureSpecs( + MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); + mState.mIsMeasuring = true; + dispatchLayoutStep2(); + // now we can get the width and height from the children. + mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); + } + } else { + if (mHasFixedSize) { + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + return; + } + // custom onMeasure + if (mAdapterUpdateDuringMeasure) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + processAdapterUpdatesAndSetAnimationFlags(); + onExitLayoutOrScroll(); + + if (mState.mRunPredictiveAnimations) { + mState.mInPreLayout = true; + } else { + // consume remaining updates to provide a consistent state with the layout pass. + mAdapterHelper.consumeUpdatesInOnePass(); + mState.mInPreLayout = false; + } + mAdapterUpdateDuringMeasure = false; + resumeRequestLayout(false); + } + + if (mAdapter != null) { + mState.mItemCount = mAdapter.getItemCount(); + } else { + mState.mItemCount = 0; + } + eatRequestLayout(); + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + resumeRequestLayout(false); + mState.mInPreLayout = false; // clear + } + } + + /** + * Used when onMeasure is called before layout manager is set + */ + void defaultOnMeasure(int widthSpec, int heightSpec) { + // calling LayoutManager here is not pretty but that API is already public and it is better + // than creating another method since this is internal. + final int width = LayoutManager.chooseSize(widthSpec, + getPaddingLeft() + getPaddingRight(), + getMinimumWidth()); + final int height = LayoutManager.chooseSize(heightSpec, + getPaddingTop() + getPaddingBottom(), + getMinimumHeight()); + + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w != oldw || h != oldh) { + invalidateGlows(); + // layout's w/h are updated during measure/layout steps. + } + } + + /** + * Sets the {@link ItemAnimator} that will handle animations involving changes + * to the items in this RecyclerView. By default, RecyclerView instantiates and + * uses an instance of {@link DefaultItemAnimator}. Whether item animations are + * enabled for the RecyclerView depends on the ItemAnimator and whether + * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations() + * supports item animations}. + * + * @param animator The ItemAnimator being set. If null, no animations will occur + * when changes occur to the items in this RecyclerView. + */ + public void setItemAnimator(ItemAnimator animator) { + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + mItemAnimator.setListener(null); + } + mItemAnimator = animator; + if (mItemAnimator != null) { + mItemAnimator.setListener(mItemAnimatorListener); + } + } + + void onEnterLayoutOrScroll() { + mLayoutOrScrollCounter++; + } + + void onExitLayoutOrScroll() { + mLayoutOrScrollCounter--; + if (mLayoutOrScrollCounter < 1) { + if (DEBUG && mLayoutOrScrollCounter < 0) { + throw new IllegalStateException("layout or scroll counter cannot go below zero." + + "Some calls are not matching"); + } + mLayoutOrScrollCounter = 0; + dispatchContentChangedIfNecessary(); + dispatchPendingImportantForAccessibilityChanges(); + } + } + + boolean isAccessibilityEnabled() { + return mAccessibilityManager != null && mAccessibilityManager.isEnabled(); + } + + private void dispatchContentChangedIfNecessary() { + final int flags = mEatenAccessibilityChangeFlags; + mEatenAccessibilityChangeFlags = 0; + if (flags != 0 && isAccessibilityEnabled()) { + final AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + event.setContentChangeTypes(flags); + sendAccessibilityEventUnchecked(event); + } + } + + /** + * Returns whether RecyclerView is currently computing a layout. + * <p> + * If this method returns true, it means that RecyclerView is in a lockdown state and any + * attempt to update adapter contents will result in an exception because adapter contents + * cannot be changed while RecyclerView is trying to compute the layout. + * <p> + * It is very unlikely that your code will be running during this state as it is + * called by the framework when a layout traversal happens or RecyclerView starts to scroll + * in response to system events (touch, accessibility etc). + * <p> + * This case may happen if you have some custom logic to change adapter contents in + * response to a View callback (e.g. focus change callback) which might be triggered during a + * layout calculation. In these cases, you should just postpone the change using a Handler or a + * similar mechanism. + * + * @return <code>true</code> if RecyclerView is currently computing a layout, <code>false</code> + * otherwise + */ + public boolean isComputingLayout() { + return mLayoutOrScrollCounter > 0; + } + + /** + * Returns true if an accessibility event should not be dispatched now. This happens when an + * accessibility request arrives while RecyclerView does not have a stable state which is very + * hard to handle for a LayoutManager. Instead, this method records necessary information about + * the event and dispatches a window change event after the critical section is finished. + * + * @return True if the accessibility event should be postponed. + */ + boolean shouldDeferAccessibilityEvent(AccessibilityEvent event) { + if (isComputingLayout()) { + int type = 0; + if (event != null) { + type = event.getContentChangeTypes(); + } + if (type == 0) { + type = AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; + } + mEatenAccessibilityChangeFlags |= type; + return true; + } + return false; + } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + if (shouldDeferAccessibilityEvent(event)) { + return; + } + super.sendAccessibilityEventUnchecked(event); + } + + /** + * Gets the current ItemAnimator for this RecyclerView. A null return value + * indicates that there is no animator and that item changes will happen without + * any animations. By default, RecyclerView instantiates and + * uses an instance of {@link DefaultItemAnimator}. + * + * @return ItemAnimator The current ItemAnimator. If null, no animations will occur + * when changes occur to the items in this RecyclerView. + */ + public ItemAnimator getItemAnimator() { + return mItemAnimator; + } + + /** + * Post a runnable to the next frame to run pending item animations. Only the first such + * request will be posted, governed by the mPostedAnimatorRunner flag. + */ + void postAnimationRunner() { + if (!mPostedAnimatorRunner && mIsAttached) { + postOnAnimation(mItemAnimatorRunner); + mPostedAnimatorRunner = true; + } + } + + private boolean predictiveItemAnimationsEnabled() { + return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations()); + } + + /** + * Consumes adapter updates and calculates which type of animations we want to run. + * Called in onMeasure and dispatchLayout. + * <p> + * This method may process only the pre-layout state of updates or all of them. + */ + private void processAdapterUpdatesAndSetAnimationFlags() { + if (mDataSetHasChangedAfterLayout) { + // Processing these items have no value since data set changed unexpectedly. + // Instead, we just reset it. + mAdapterHelper.reset(); + mLayout.onItemsChanged(this); + } + // simple animations are a subset of advanced animations (which will cause a + // pre-layout step) + // If layout supports predictive animations, pre-process to decide if we want to run them + if (predictiveItemAnimationsEnabled()) { + mAdapterHelper.preProcess(); + } else { + mAdapterHelper.consumeUpdatesInOnePass(); + } + boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; + mState.mRunSimpleAnimations = mFirstLayoutComplete + && mItemAnimator != null + && (mDataSetHasChangedAfterLayout + || animationTypeSupported + || mLayout.mRequestedSimpleAnimations) + && (!mDataSetHasChangedAfterLayout + || mAdapter.hasStableIds()); + mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations + && animationTypeSupported + && !mDataSetHasChangedAfterLayout + && predictiveItemAnimationsEnabled(); + } + + /** + * Wrapper around layoutChildren() that handles animating changes caused by layout. + * Animations work on the assumption that there are five different kinds of items + * in play: + * PERSISTENT: items are visible before and after layout + * REMOVED: items were visible before layout and were removed by the app + * ADDED: items did not exist before layout and were added by the app + * DISAPPEARING: items exist in the data set before/after, but changed from + * visible to non-visible in the process of layout (they were moved off + * screen as a side-effect of other changes) + * APPEARING: items exist in the data set before/after, but changed from + * non-visible to visible in the process of layout (they were moved on + * screen as a side-effect of other changes) + * The overall approach figures out what items exist before/after layout and + * infers one of the five above states for each of the items. Then the animations + * are set up accordingly: + * PERSISTENT views are animated via + * {@link ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * DISAPPEARING views are animated via + * {@link ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * APPEARING views are animated via + * {@link ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * and changed views are animated via + * {@link ItemAnimator#animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)}. + */ + void dispatchLayout() { + if (mAdapter == null) { + Log.e(TAG, "No adapter attached; skipping layout"); + // leave the state in START + return; + } + if (mLayout == null) { + Log.e(TAG, "No layout manager attached; skipping layout"); + // leave the state in START + return; + } + mState.mIsMeasuring = false; + if (mState.mLayoutStep == State.STEP_START) { + dispatchLayoutStep1(); + mLayout.setExactMeasureSpecsFrom(this); + dispatchLayoutStep2(); + } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() + || mLayout.getHeight() != getHeight()) { + // First 2 steps are done in onMeasure but looks like we have to run again due to + // changed size. + mLayout.setExactMeasureSpecsFrom(this); + dispatchLayoutStep2(); + } else { + // always make sure we sync them (to ensure mode is exact) + mLayout.setExactMeasureSpecsFrom(this); + } + dispatchLayoutStep3(); + } + + private void saveFocusInfo() { + View child = null; + if (mPreserveFocusAfterLayout && hasFocus() && mAdapter != null) { + child = getFocusedChild(); + } + + final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child); + if (focusedVh == null) { + resetFocusInfo(); + } else { + mState.mFocusedItemId = mAdapter.hasStableIds() ? focusedVh.getItemId() : NO_ID; + // mFocusedItemPosition should hold the current adapter position of the previously + // focused item. If the item is removed, we store the previous adapter position of the + // removed item. + mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION + : (focusedVh.isRemoved() ? focusedVh.mOldPosition + : focusedVh.getAdapterPosition()); + mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView); + } + } + + private void resetFocusInfo() { + mState.mFocusedItemId = NO_ID; + mState.mFocusedItemPosition = NO_POSITION; + mState.mFocusedSubChildId = View.NO_ID; + } + + /** + * Finds the best view candidate to request focus on using mFocusedItemPosition index of the + * previously focused item. It first traverses the adapter forward to find a focusable candidate + * and if no such candidate is found, it reverses the focus search direction for the items + * before the mFocusedItemPosition'th index; + * @return The best candidate to request focus on, or null if no such candidate exists. Null + * indicates all the existing adapter items are unfocusable. + */ + @Nullable + private View findNextViewToFocus() { + int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition + : 0; + ViewHolder nextFocus; + final int itemCount = mState.getItemCount(); + for (int i = startFocusSearchIndex; i < itemCount; i++) { + nextFocus = findViewHolderForAdapterPosition(i); + if (nextFocus == null) { + break; + } + if (nextFocus.itemView.hasFocusable()) { + return nextFocus.itemView; + } + } + final int limit = Math.min(itemCount, startFocusSearchIndex); + for (int i = limit - 1; i >= 0; i--) { + nextFocus = findViewHolderForAdapterPosition(i); + if (nextFocus == null) { + return null; + } + if (nextFocus.itemView.hasFocusable()) { + return nextFocus.itemView; + } + } + return null; + } + + private void recoverFocusFromState() { + if (!mPreserveFocusAfterLayout || mAdapter == null || !hasFocus() + || getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS + || (getDescendantFocusability() == FOCUS_BEFORE_DESCENDANTS && isFocused())) { + // No-op if either of these cases happens: + // 1. RV has no focus, or 2. RV blocks focus to its children, or 3. RV takes focus + // before its children and is focused (i.e. it already stole the focus away from its + // descendants). + return; + } + // only recover focus if RV itself has the focus or the focused view is hidden + if (!isFocused()) { + final View focusedChild = getFocusedChild(); + if (IGNORE_DETACHED_FOCUSED_CHILD + && (focusedChild.getParent() == null || !focusedChild.hasFocus())) { + // Special handling of API 15-. A focused child can be invalid because mFocus is not + // cleared when the child is detached (mParent = null), + // This happens because clearFocus on API 15- does not invalidate mFocus of its + // parent when this child is detached. + // For API 16+, this is not an issue because requestFocus takes care of clearing the + // prior detached focused child. For API 15- the problem happens in 2 cases because + // clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called + // for the current focused item which calls clearChild or 2. when the prior focused + // child is removed, removeDetachedView called in layout step 3 which calls + // clearChild. We should ignore this invalid focused child in all our calculations + // for the next view to receive focus, and apply the focus recovery logic instead. + if (mChildHelper.getChildCount() == 0) { + // No children left. Request focus on the RV itself since one of its children + // was holding focus previously. + requestFocus(); + return; + } + } else if (!mChildHelper.isHidden(focusedChild)) { + // If the currently focused child is hidden, apply the focus recovery logic. + // Otherwise return, i.e. the currently (unhidden) focused child is good enough :/. + return; + } + } + ViewHolder focusTarget = null; + // RV first attempts to locate the previously focused item to request focus on using + // mFocusedItemId. If such an item no longer exists, it then makes a best-effort attempt to + // find the next best candidate to request focus on based on mFocusedItemPosition. + if (mState.mFocusedItemId != NO_ID && mAdapter.hasStableIds()) { + focusTarget = findViewHolderForItemId(mState.mFocusedItemId); + } + View viewToFocus = null; + if (focusTarget == null || mChildHelper.isHidden(focusTarget.itemView) + || !focusTarget.itemView.hasFocusable()) { + if (mChildHelper.getChildCount() > 0) { + // At this point, RV has focus and either of these conditions are true: + // 1. There's no previously focused item either because RV received focused before + // layout, or the previously focused item was removed, or RV doesn't have stable IDs + // 2. Previous focus child is hidden, or 3. Previous focused child is no longer + // focusable. In either of these cases, we make sure that RV still passes down the + // focus to one of its focusable children using a best-effort algorithm. + viewToFocus = findNextViewToFocus(); + } + } else { + // looks like the focused item has been replaced with another view that represents the + // same item in the adapter. Request focus on that. + viewToFocus = focusTarget.itemView; + } + + if (viewToFocus != null) { + if (mState.mFocusedSubChildId != NO_ID) { + View child = viewToFocus.findViewById(mState.mFocusedSubChildId); + if (child != null && child.isFocusable()) { + viewToFocus = child; + } + } + viewToFocus.requestFocus(); + } + } + + private int getDeepestFocusedViewWithId(View view) { + int lastKnownId = view.getId(); + while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) { + view = ((ViewGroup) view).getFocusedChild(); + final int id = view.getId(); + if (id != View.NO_ID) { + lastKnownId = view.getId(); + } + } + return lastKnownId; + } + + /** + * The first step of a layout where we; + * - process adapter updates + * - decide which animation should run + * - save information about current views + * - If necessary, run predictive layout and save its information + */ + private void dispatchLayoutStep1() { + mState.assertLayoutStep(State.STEP_START); + mState.mIsMeasuring = false; + eatRequestLayout(); + mViewInfoStore.clear(); + onEnterLayoutOrScroll(); + processAdapterUpdatesAndSetAnimationFlags(); + saveFocusInfo(); + mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; + mItemsAddedOrRemoved = mItemsChanged = false; + mState.mInPreLayout = mState.mRunPredictiveAnimations; + mState.mItemCount = mAdapter.getItemCount(); + findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); + + if (mState.mRunSimpleAnimations) { + // Step 0: Find out where all non-removed items are, pre-layout + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) { + continue; + } + final ItemHolderInfo animationInfo = mItemAnimator + .recordPreLayoutInformation(mState, holder, + ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), + holder.getUnmodifiedPayloads()); + mViewInfoStore.addToPreLayout(holder, animationInfo); + if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved() + && !holder.shouldIgnore() && !holder.isInvalid()) { + long key = getChangedHolderKey(holder); + // This is NOT the only place where a ViewHolder is added to old change holders + // list. There is another case where: + // * A VH is currently hidden but not deleted + // * The hidden item is changed in the adapter + // * Layout manager decides to layout the item in the pre-Layout pass (step1) + // When this case is detected, RV will un-hide that view and add to the old + // change holders list. + mViewInfoStore.addToOldChangeHolders(key, holder); + } + } + } + if (mState.mRunPredictiveAnimations) { + // Step 1: run prelayout: This will use the old positions of items. The layout manager + // is expected to layout everything, even removed items (though not to add removed + // items back to the container). This gives the pre-layout position of APPEARING views + // which come into existence as part of the real layout. + + // Save old positions so that LayoutManager can run its mapping logic. + saveOldPositions(); + final boolean didStructureChange = mState.mStructureChanged; + mState.mStructureChanged = false; + // temporarily disable flag because we are asking for previous layout + mLayout.onLayoutChildren(mRecycler, mState); + mState.mStructureChanged = didStructureChange; + + for (int i = 0; i < mChildHelper.getChildCount(); ++i) { + final View child = mChildHelper.getChildAt(i); + final ViewHolder viewHolder = getChildViewHolderInt(child); + if (viewHolder.shouldIgnore()) { + continue; + } + if (!mViewInfoStore.isInPreLayout(viewHolder)) { + int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder); + boolean wasHidden = viewHolder + .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (!wasHidden) { + flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; + } + final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( + mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); + if (wasHidden) { + recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); + } else { + mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); + } + } + } + // we don't process disappearing list because they may re-appear in post layout pass. + clearOldPositions(); + } else { + clearOldPositions(); + } + onExitLayoutOrScroll(); + resumeRequestLayout(false); + mState.mLayoutStep = State.STEP_LAYOUT; + } + + /** + * The second layout step where we do the actual layout of the views for the final state. + * This step might be run multiple times if necessary (e.g. measure). + */ + private void dispatchLayoutStep2() { + eatRequestLayout(); + onEnterLayoutOrScroll(); + mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); + mAdapterHelper.consumeUpdatesInOnePass(); + mState.mItemCount = mAdapter.getItemCount(); + mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; + + // Step 2: Run layout + mState.mInPreLayout = false; + mLayout.onLayoutChildren(mRecycler, mState); + + mState.mStructureChanged = false; + mPendingSavedState = null; + + // onLayoutChildren may have caused client code to disable item animations; re-check + mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; + mState.mLayoutStep = State.STEP_ANIMATIONS; + onExitLayoutOrScroll(); + resumeRequestLayout(false); + } + + /** + * The final step of the layout where we save the information about views for animations, + * trigger animations and do any necessary cleanup. + */ + private void dispatchLayoutStep3() { + mState.assertLayoutStep(State.STEP_ANIMATIONS); + eatRequestLayout(); + onEnterLayoutOrScroll(); + mState.mLayoutStep = State.STEP_START; + if (mState.mRunSimpleAnimations) { + // Step 3: Find out where things are now, and process change animations. + // traverse list in reverse because we may call animateChange in the loop which may + // remove the target view holder. + for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { + ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + long key = getChangedHolderKey(holder); + final ItemHolderInfo animationInfo = mItemAnimator + .recordPostLayoutInformation(mState, holder); + ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); + if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { + // run a change animation + + // If an Item is CHANGED but the updated version is disappearing, it creates + // a conflicting case. + // Since a view that is marked as disappearing is likely to be going out of + // bounds, we run a change animation. Both views will be cleaned automatically + // once their animations finish. + // On the other hand, if it is the same view holder instance, we run a + // disappearing animation instead because we are not going to rebind the updated + // VH unless it is enforced by the layout manager. + final boolean oldDisappearing = mViewInfoStore.isDisappearing( + oldChangeViewHolder); + final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); + if (oldDisappearing && oldChangeViewHolder == holder) { + // run disappear animation instead of change + mViewInfoStore.addToPostLayout(holder, animationInfo); + } else { + final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( + oldChangeViewHolder); + // we add and remove so that any post info is merged. + mViewInfoStore.addToPostLayout(holder, animationInfo); + ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); + if (preInfo == null) { + handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); + } else { + animateChange(oldChangeViewHolder, holder, preInfo, postInfo, + oldDisappearing, newDisappearing); + } + } + } else { + mViewInfoStore.addToPostLayout(holder, animationInfo); + } + } + + // Step 4: Process view info lists and trigger animations + mViewInfoStore.process(mViewInfoProcessCallback); + } + + mLayout.removeAndRecycleScrapInt(mRecycler); + mState.mPreviousLayoutItemCount = mState.mItemCount; + mDataSetHasChangedAfterLayout = false; + mState.mRunSimpleAnimations = false; + + mState.mRunPredictiveAnimations = false; + mLayout.mRequestedSimpleAnimations = false; + if (mRecycler.mChangedScrap != null) { + mRecycler.mChangedScrap.clear(); + } + if (mLayout.mPrefetchMaxObservedInInitialPrefetch) { + // Initial prefetch has expanded cache, so reset until next prefetch. + // This prevents initial prefetches from expanding the cache permanently. + mLayout.mPrefetchMaxCountObserved = 0; + mLayout.mPrefetchMaxObservedInInitialPrefetch = false; + mRecycler.updateViewCacheSize(); + } + + mLayout.onLayoutCompleted(mState); + onExitLayoutOrScroll(); + resumeRequestLayout(false); + mViewInfoStore.clear(); + if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { + dispatchOnScrolled(0, 0); + } + recoverFocusFromState(); + resetFocusInfo(); + } + + /** + * This handles the case where there is an unexpected VH missing in the pre-layout map. + * <p> + * We might be able to detect the error in the application which will help the developer to + * resolve the issue. + * <p> + * If it is not an expected error, we at least print an error to notify the developer and ignore + * the animation. + * + * https://code.google.com/p/android/issues/detail?id=193958 + * + * @param key The change key + * @param holder Current ViewHolder + * @param oldChangeViewHolder Changed ViewHolder + */ + private void handleMissingPreInfoForChangeError(long key, + ViewHolder holder, ViewHolder oldChangeViewHolder) { + // check if two VH have the same key, if so, print that as an error + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = mChildHelper.getChildAt(i); + ViewHolder other = getChildViewHolderInt(view); + if (other == holder) { + continue; + } + final long otherKey = getChangedHolderKey(other); + if (otherKey == key) { + if (mAdapter != null && mAdapter.hasStableIds()) { + throw new IllegalStateException("Two different ViewHolders have the same stable" + + " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT" + + " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder); + } else { + throw new IllegalStateException("Two different ViewHolders have the same change" + + " ID. This might happen due to inconsistent Adapter update events or" + + " if the LayoutManager lays out the same View multiple times." + + "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder); + } + } + } + // Very unlikely to happen but if it does, notify the developer. + Log.e(TAG, "Problem while matching changed view holders with the new" + + "ones. The pre-layout information for the change holder " + oldChangeViewHolder + + " cannot be found but it is necessary for " + holder); + } + + /** + * Records the animation information for a view holder that was bounced from hidden list. It + * also clears the bounce back flag. + */ + void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder, + ItemHolderInfo animationInfo) { + // looks like this view bounced back from hidden list! + viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (mState.mTrackOldChangeHolders && viewHolder.isUpdated() + && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) { + long key = getChangedHolderKey(viewHolder); + mViewInfoStore.addToOldChangeHolders(key, viewHolder); + } + mViewInfoStore.addToPreLayout(viewHolder, animationInfo); + } + + private void findMinMaxChildLayoutPositions(int[] into) { + final int count = mChildHelper.getChildCount(); + if (count == 0) { + into[0] = NO_POSITION; + into[1] = NO_POSITION; + return; + } + int minPositionPreLayout = Integer.MAX_VALUE; + int maxPositionPreLayout = Integer.MIN_VALUE; + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + final int pos = holder.getLayoutPosition(); + if (pos < minPositionPreLayout) { + minPositionPreLayout = pos; + } + if (pos > maxPositionPreLayout) { + maxPositionPreLayout = pos; + } + } + into[0] = minPositionPreLayout; + into[1] = maxPositionPreLayout; + } + + private boolean didChildRangeChange(int minPositionPreLayout, int maxPositionPreLayout) { + findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); + return mMinMaxLayoutPositions[0] != minPositionPreLayout + || mMinMaxLayoutPositions[1] != maxPositionPreLayout; + } + + @Override + protected void removeDetachedView(View child, boolean animate) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + if (vh.isTmpDetached()) { + vh.clearTmpDetachFlag(); + } else if (!vh.shouldIgnore()) { + throw new IllegalArgumentException("Called removeDetachedView with a view which" + + " is not flagged as tmp detached." + vh); + } + } + dispatchChildDetached(child); + super.removeDetachedView(child, animate); + } + + /** + * Returns a unique key to be used while handling change animations. + * It might be child's position or stable id depending on the adapter type. + */ + long getChangedHolderKey(ViewHolder holder) { + return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition; + } + + void animateAppearance(@NonNull ViewHolder itemHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + itemHolder.setIsRecyclable(false); + if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) { + postAnimationRunner(); + } + } + + void animateDisappearance(@NonNull ViewHolder holder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + addAnimatingView(holder); + holder.setIsRecyclable(false); + if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { + postAnimationRunner(); + } + } + + private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, + boolean oldHolderDisappearing, boolean newHolderDisappearing) { + oldHolder.setIsRecyclable(false); + if (oldHolderDisappearing) { + addAnimatingView(oldHolder); + } + if (oldHolder != newHolder) { + if (newHolderDisappearing) { + addAnimatingView(newHolder); + } + oldHolder.mShadowedHolder = newHolder; + // old holder should disappear after animation ends + addAnimatingView(oldHolder); + mRecycler.unscrapView(oldHolder); + newHolder.setIsRecyclable(false); + newHolder.mShadowingHolder = oldHolder; + } + if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + Trace.beginSection(TRACE_ON_LAYOUT_TAG); + dispatchLayout(); + Trace.endSection(); + mFirstLayoutComplete = true; + } + + @Override + public void requestLayout() { + if (mEatRequestLayout == 0 && !mLayoutFrozen) { + super.requestLayout(); + } else { + mLayoutRequestEaten = true; + } + } + + void markItemDecorInsetsDirty() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = mChildHelper.getUnfilteredChildAt(i); + ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; + } + mRecycler.markItemDecorInsetsDirty(); + } + + @Override + public void draw(Canvas c) { + super.draw(c); + + final int count = mItemDecorations.size(); + for (int i = 0; i < count; i++) { + mItemDecorations.get(i).onDrawOver(c, this, mState); + } + // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we + // need find children closest to edges. Not sure if it is worth the effort. + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished()) { + final int restore = c.save(); + final int padding = mClipToPadding ? getPaddingBottom() : 0; + c.rotate(270); + c.translate(-getHeight() + padding, 0); + needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c); + c.restoreToCount(restore); + } + if (mTopGlow != null && !mTopGlow.isFinished()) { + final int restore = c.save(); + if (mClipToPadding) { + c.translate(getPaddingLeft(), getPaddingTop()); + } + needsInvalidate |= mTopGlow != null && mTopGlow.draw(c); + c.restoreToCount(restore); + } + if (mRightGlow != null && !mRightGlow.isFinished()) { + final int restore = c.save(); + final int width = getWidth(); + final int padding = mClipToPadding ? getPaddingTop() : 0; + c.rotate(90); + c.translate(-padding, -width); + needsInvalidate |= mRightGlow != null && mRightGlow.draw(c); + c.restoreToCount(restore); + } + if (mBottomGlow != null && !mBottomGlow.isFinished()) { + final int restore = c.save(); + c.rotate(180); + if (mClipToPadding) { + c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom()); + } else { + c.translate(-getWidth(), -getHeight()); + } + needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c); + c.restoreToCount(restore); + } + + // If some views are animating, ItemDecorators are likely to move/change with them. + // Invalidate RecyclerView to re-draw decorators. This is still efficient because children's + // display lists are not invalidated. + if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0 + && mItemAnimator.isRunning()) { + needsInvalidate = true; + } + + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + + final int count = mItemDecorations.size(); + for (int i = 0; i < count; i++) { + mItemDecorations.get(i).onDraw(c, this, mState); + } + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + return mLayout.generateDefaultLayoutParams(); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + return mLayout.generateLayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + return mLayout.generateLayoutParams(p); + } + + /** + * Returns true if RecyclerView is currently running some animations. + * <p> + * If you want to be notified when animations are finished, use + * {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}. + * + * @return True if there are some item animations currently running or waiting to be started. + */ + public boolean isAnimating() { + return mItemAnimator != null && mItemAnimator.isRunning(); + } + + void saveOldPositions() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) { + throw new IllegalStateException("view holder cannot have position -1 unless it" + + " is removed"); + } + if (!holder.shouldIgnore()) { + holder.saveOldPosition(); + } + } + } + + void clearOldPositions() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (!holder.shouldIgnore()) { + holder.clearOldPosition(); + } + } + mRecycler.clearOldPositions(); + } + + void offsetPositionRecordsForMove(int from, int to) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int start, end, inBetweenOffset; + if (from < to) { + start = from; + end = to; + inBetweenOffset = -1; + } else { + start = to; + end = from; + inBetweenOffset = 1; + } + + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder == null || holder.mPosition < start || holder.mPosition > end) { + continue; + } + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder " + + holder); + } + if (holder.mPosition == from) { + holder.offsetPosition(to - from, false); + } else { + holder.offsetPosition(inBetweenOffset, false); + } + + mState.mStructureChanged = true; + } + mRecycler.offsetPositionRecordsForMove(from, to); + requestLayout(); + } + + void offsetPositionRecordsForInsert(int positionStart, int itemCount) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder " + + holder + " now at position " + (holder.mPosition + itemCount)); + } + holder.offsetPosition(itemCount, false); + mState.mStructureChanged = true; + } + } + mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount); + requestLayout(); + } + + void offsetPositionRecordsForRemove(int positionStart, int itemCount, + boolean applyToPreLayout) { + final int positionEnd = positionStart + itemCount; + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore()) { + if (holder.mPosition >= positionEnd) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i + + " holder " + holder + " now at position " + + (holder.mPosition - itemCount)); + } + holder.offsetPosition(-itemCount, applyToPreLayout); + mState.mStructureChanged = true; + } else if (holder.mPosition >= positionStart) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i + + " holder " + holder + " now REMOVED"); + } + holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount, + applyToPreLayout); + mState.mStructureChanged = true; + } + } + } + mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout); + requestLayout(); + } + + /** + * Rebind existing views for the given range, or create as needed. + * + * @param positionStart Adapter position to start at + * @param itemCount Number of views that must explicitly be rebound + */ + void viewRangeUpdate(int positionStart, int itemCount, Object payload) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int positionEnd = positionStart + itemCount; + + for (int i = 0; i < childCount; i++) { + final View child = mChildHelper.getUnfilteredChildAt(i); + final ViewHolder holder = getChildViewHolderInt(child); + if (holder == null || holder.shouldIgnore()) { + continue; + } + if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) { + // We re-bind these view holders after pre-processing is complete so that + // ViewHolders have their final positions assigned. + holder.addFlags(ViewHolder.FLAG_UPDATE); + holder.addChangePayload(payload); + // lp cannot be null since we get ViewHolder from it. + ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; + } + } + mRecycler.viewRangeUpdate(positionStart, itemCount); + } + + boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) { + return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder, + viewHolder.getUnmodifiedPayloads()); + } + + + /** + * Call this method to signal that *all* adapter content has changed (generally, because of + * swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should + * be discarded or animated. Note that this work is deferred because RecyclerView requires a + * layout to resolve non-incremental changes to the data set. + * + * Attached items are labeled as position unknown, and may no longer be cached. + * + * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true, + * so calling this method *must* be associated with marking the cache invalid, so that the + * only valid items that remain in the cache, once layout occurs, are prefetched items. + */ + void setDataSetChangedAfterLayout() { + if (mDataSetHasChangedAfterLayout) { + return; + } + mDataSetHasChangedAfterLayout = true; + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore()) { + holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + } + } + mRecycler.setAdapterPositionsAsUnknown(); + + // immediately mark all views as invalid, so prefetched views can be + // differentiated from views bound to previous data set - both in children, and cache + markKnownViewsInvalid(); + } + + /** + * Mark all known views as invalid. Used in response to a, "the whole world might have changed" + * data change event. + */ + void markKnownViewsInvalid() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore()) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + } + } + markItemDecorInsetsDirty(); + mRecycler.markKnownViewsInvalid(); + } + + /** + * Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method + * will trigger a {@link #requestLayout()} call. + */ + public void invalidateItemDecorations() { + if (mItemDecorations.size() == 0) { + return; + } + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot invalidate item decorations during a scroll" + + " or layout"); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Returns true if the RecyclerView should attempt to preserve currently focused Adapter Item's + * focus even if the View representing the Item is replaced during a layout calculation. + * <p> + * By default, this value is {@code true}. + * + * @return True if the RecyclerView will try to preserve focused Item after a layout if it loses + * focus. + * + * @see #setPreserveFocusAfterLayout(boolean) + */ + public boolean getPreserveFocusAfterLayout() { + return mPreserveFocusAfterLayout; + } + + /** + * Set whether the RecyclerView should try to keep the same Item focused after a layout + * calculation or not. + * <p> + * Usually, LayoutManagers keep focused views visible before and after layout but sometimes, + * views may lose focus during a layout calculation as their state changes or they are replaced + * with another view due to type change or animation. In these cases, RecyclerView can request + * focus on the new view automatically. + * + * @param preserveFocusAfterLayout Whether RecyclerView should preserve focused Item during a + * layout calculations. Defaults to true. + * + * @see #getPreserveFocusAfterLayout() + */ + public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) { + mPreserveFocusAfterLayout = preserveFocusAfterLayout; + } + + /** + * Retrieve the {@link ViewHolder} for the given child view. + * + * @param child Child of this RecyclerView to query for its ViewHolder + * @return The child view's ViewHolder + */ + public ViewHolder getChildViewHolder(View child) { + final ViewParent parent = child.getParent(); + if (parent != null && parent != this) { + throw new IllegalArgumentException("View " + child + " is not a direct child of " + + this); + } + return getChildViewHolderInt(child); + } + + /** + * Traverses the ancestors of the given view and returns the item view that contains it and + * also a direct child of the RecyclerView. This returned view can be used to get the + * ViewHolder by calling {@link #getChildViewHolder(View)}. + * + * @param view The view that is a descendant of the RecyclerView. + * + * @return The direct child of the RecyclerView which contains the given view or null if the + * provided view is not a descendant of this RecyclerView. + * + * @see #getChildViewHolder(View) + * @see #findContainingViewHolder(View) + */ + @Nullable + public View findContainingItemView(View view) { + ViewParent parent = view.getParent(); + while (parent != null && parent != this && parent instanceof View) { + view = (View) parent; + parent = view.getParent(); + } + return parent == this ? view : null; + } + + /** + * Returns the ViewHolder that contains the given view. + * + * @param view The view that is a descendant of the RecyclerView. + * + * @return The ViewHolder that contains the given view or null if the provided view is not a + * descendant of this RecyclerView. + */ + @Nullable + public ViewHolder findContainingViewHolder(View view) { + View itemView = findContainingItemView(view); + return itemView == null ? null : getChildViewHolder(itemView); + } + + + static ViewHolder getChildViewHolderInt(View child) { + if (child == null) { + return null; + } + return ((LayoutParams) child.getLayoutParams()).mViewHolder; + } + + /** + * @deprecated use {@link #getChildAdapterPosition(View)} or + * {@link #getChildLayoutPosition(View)}. + */ + @Deprecated + public int getChildPosition(View child) { + return getChildAdapterPosition(child); + } + + /** + * Return the adapter position that the given child view corresponds to. + * + * @param child Child View to query + * @return Adapter position corresponding to the given view or {@link #NO_POSITION} + */ + public int getChildAdapterPosition(View child) { + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getAdapterPosition() : NO_POSITION; + } + + /** + * Return the adapter position of the given child view as of the latest completed layout pass. + * <p> + * This position may not be equal to Item's adapter position if there are pending changes + * in the adapter which have not been reflected to the layout yet. + * + * @param child Child View to query + * @return Adapter position of the given View as of last layout pass or {@link #NO_POSITION} if + * the View is representing a removed item. + */ + public int getChildLayoutPosition(View child) { + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getLayoutPosition() : NO_POSITION; + } + + /** + * Return the stable item id that the given child view corresponds to. + * + * @param child Child View to query + * @return Item id corresponding to the given view or {@link #NO_ID} + */ + public long getChildItemId(View child) { + if (mAdapter == null || !mAdapter.hasStableIds()) { + return NO_ID; + } + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getItemId() : NO_ID; + } + + /** + * @deprecated use {@link #findViewHolderForLayoutPosition(int)} or + * {@link #findViewHolderForAdapterPosition(int)} + */ + @Deprecated + public ViewHolder findViewHolderForPosition(int position) { + return findViewHolderForPosition(position, false); + } + + /** + * Return the ViewHolder for the item in the given position of the data set as of the latest + * layout pass. + * <p> + * This method checks only the children of RecyclerView. If the item at the given + * <code>position</code> is not laid out, it <em>will not</em> create a new one. + * <p> + * Note that when Adapter contents change, ViewHolder positions are not updated until the + * next layout calculation. If there are pending adapter updates, the return value of this + * method may not match your adapter contents. You can use + * #{@link ViewHolder#getAdapterPosition()} to get the current adapter position of a ViewHolder. + * <p> + * When the ItemAnimator is running a change animation, there might be 2 ViewHolders + * with the same layout position representing the same Item. In this case, the updated + * ViewHolder will be returned. + * + * @param position The position of the item in the data set of the adapter + * @return The ViewHolder at <code>position</code> or null if there is no such item + */ + public ViewHolder findViewHolderForLayoutPosition(int position) { + return findViewHolderForPosition(position, false); + } + + /** + * Return the ViewHolder for the item in the given position of the data set. Unlike + * {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending + * adapter changes that may not be reflected to the layout yet. On the other hand, if + * {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been + * calculated yet, this method will return <code>null</code> since the new positions of views + * are unknown until the layout is calculated. + * <p> + * This method checks only the children of RecyclerView. If the item at the given + * <code>position</code> is not laid out, it <em>will not</em> create a new one. + * <p> + * When the ItemAnimator is running a change animation, there might be 2 ViewHolders + * representing the same Item. In this case, the updated ViewHolder will be returned. + * + * @param position The position of the item in the data set of the adapter + * @return The ViewHolder at <code>position</code> or null if there is no such item + */ + public ViewHolder findViewHolderForAdapterPosition(int position) { + if (mDataSetHasChangedAfterLayout) { + return null; + } + final int childCount = mChildHelper.getUnfilteredChildCount(); + // hidden VHs are not preferred but if that is the only one we find, we rather return it + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved() + && getAdapterPositionFor(holder) == position) { + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + return hidden; + } + + ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved()) { + if (checkNewPosition) { + if (holder.mPosition != position) { + continue; + } + } else if (holder.getLayoutPosition() != position) { + continue; + } + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + // This method should not query cached views. It creates a problem during adapter updates + // when we are dealing with already laid out views. Also, for the public method, it is more + // reasonable to return null if position is not laid out. + return hidden; + } + + /** + * Return the ViewHolder for the item with the given id. The RecyclerView must + * use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to + * return a non-null value. + * <p> + * This method checks only the children of RecyclerView. If the item with the given + * <code>id</code> is not laid out, it <em>will not</em> create a new one. + * + * When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the + * same id. In this case, the updated ViewHolder will be returned. + * + * @param id The id for the requested item + * @return The ViewHolder with the given <code>id</code> or null if there is no such item + */ + public ViewHolder findViewHolderForItemId(long id) { + if (mAdapter == null || !mAdapter.hasStableIds()) { + return null; + } + final int childCount = mChildHelper.getUnfilteredChildCount(); + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved() && holder.getItemId() == id) { + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + return hidden; + } + + /** + * Find the topmost view under the given point. + * + * @param x Horizontal position in pixels to search + * @param y Vertical position in pixels to search + * @return The child view under (x, y) or null if no matching child is found + */ + public View findChildViewUnder(float x, float y) { + final int count = mChildHelper.getChildCount(); + for (int i = count - 1; i >= 0; i--) { + final View child = mChildHelper.getChildAt(i); + final float translationX = child.getTranslationX(); + final float translationY = child.getTranslationY(); + if (x >= child.getLeft() + translationX + && x <= child.getRight() + translationX + && y >= child.getTop() + translationY + && y <= child.getBottom() + translationY) { + return child; + } + } + return null; + } + + @Override + public boolean drawChild(Canvas canvas, View child, long drawingTime) { + return super.drawChild(canvas, child, drawingTime); + } + + /** + * Offset the bounds of all child views by <code>dy</code> pixels. + * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}. + * + * @param dy Vertical pixel offset to apply to the bounds of all child views + */ + public void offsetChildrenVertical(int dy) { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + mChildHelper.getChildAt(i).offsetTopAndBottom(dy); + } + } + + /** + * Called when an item view is attached to this RecyclerView. + * + * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications + * of child views as they become attached. This will be called before a + * {@link LayoutManager} measures or lays out the view and is a good time to perform these + * changes.</p> + * + * @param child Child view that is now attached to this RecyclerView and its associated window + */ + public void onChildAttachedToWindow(View child) { + } + + /** + * Called when an item view is detached from this RecyclerView. + * + * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications + * of child views as they become detached. This will be called as a + * {@link LayoutManager} fully detaches the child view from the parent and its window.</p> + * + * @param child Child view that is now detached from this RecyclerView and its associated window + */ + public void onChildDetachedFromWindow(View child) { + } + + /** + * Offset the bounds of all child views by <code>dx</code> pixels. + * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}. + * + * @param dx Horizontal pixel offset to apply to the bounds of all child views + */ + public void offsetChildrenHorizontal(int dx) { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + mChildHelper.getChildAt(i).offsetLeftAndRight(dx); + } + } + + /** + * Returns the bounds of the view including its decoration and margins. + * + * @param view The view element to check + * @param outBounds A rect that will receive the bounds of the element including its + * decoration and margins. + */ + public void getDecoratedBoundsWithMargins(View view, Rect outBounds) { + getDecoratedBoundsWithMarginsInt(view, outBounds); + } + + static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) { + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + outBounds.set(view.getLeft() - insets.left - lp.leftMargin, + view.getTop() - insets.top - lp.topMargin, + view.getRight() + insets.right + lp.rightMargin, + view.getBottom() + insets.bottom + lp.bottomMargin); + } + + Rect getItemDecorInsetsForChild(View child) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.mInsetsDirty) { + return lp.mDecorInsets; + } + + if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) { + // changed/invalid items should not be updated until they are rebound. + return lp.mDecorInsets; + } + final Rect insets = lp.mDecorInsets; + insets.set(0, 0, 0, 0); + final int decorCount = mItemDecorations.size(); + for (int i = 0; i < decorCount; i++) { + mTempRect.set(0, 0, 0, 0); + mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); + insets.left += mTempRect.left; + insets.top += mTempRect.top; + insets.right += mTempRect.right; + insets.bottom += mTempRect.bottom; + } + lp.mInsetsDirty = false; + return insets; + } + + /** + * Called when the scroll position of this RecyclerView changes. Subclasses should use + * this method to respond to scrolling within the adapter's data set instead of an explicit + * listener. + * + * <p>This method will always be invoked before listeners. If a subclass needs to perform + * any additional upkeep or bookkeeping after scrolling but before listeners run, + * this is a good place to do so.</p> + * + * <p>This differs from {@link View#onScrollChanged(int, int, int, int)} in that it receives + * the distance scrolled in either direction within the adapter's data set instead of absolute + * scroll coordinates. Since RecyclerView cannot compute the absolute scroll position from + * any arbitrary point in the data set, <code>onScrollChanged</code> will always receive + * the current {@link View#getScrollX()} and {@link View#getScrollY()} values which + * do not correspond to the data set scroll position. However, some subclasses may choose + * to use these fields as special offsets.</p> + * + * @param dx horizontal distance scrolled in pixels + * @param dy vertical distance scrolled in pixels + */ + public void onScrolled(int dx, int dy) { + // Do nothing + } + + void dispatchOnScrolled(int hresult, int vresult) { + mDispatchScrollCounter++; + // Pass the current scrollX/scrollY values; no actual change in these properties occurred + // but some general-purpose code may choose to respond to changes this way. + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + onScrollChanged(scrollX, scrollY, scrollX, scrollY); + + // Pass the real deltas to onScrolled, the RecyclerView-specific method. + onScrolled(hresult, vresult); + + // Invoke listeners last. Subclassed view methods always handle the event first. + // All internal state is consistent by the time listeners are invoked. + if (mScrollListener != null) { + mScrollListener.onScrolled(this, hresult, vresult); + } + if (mScrollListeners != null) { + for (int i = mScrollListeners.size() - 1; i >= 0; i--) { + mScrollListeners.get(i).onScrolled(this, hresult, vresult); + } + } + mDispatchScrollCounter--; + } + + /** + * Called when the scroll state of this RecyclerView changes. Subclasses should use this + * method to respond to state changes instead of an explicit listener. + * + * <p>This method will always be invoked before listeners, but after the LayoutManager + * responds to the scroll state change.</p> + * + * @param state the new scroll state, one of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING} + */ + public void onScrollStateChanged(int state) { + // Do nothing + } + + void dispatchOnScrollStateChanged(int state) { + // Let the LayoutManager go first; this allows it to bring any properties into + // a consistent state before the RecyclerView subclass responds. + if (mLayout != null) { + mLayout.onScrollStateChanged(state); + } + + // Let the RecyclerView subclass handle this event next; any LayoutManager property + // changes will be reflected by this time. + onScrollStateChanged(state); + + // Listeners go last. All other internal state is consistent by this point. + if (mScrollListener != null) { + mScrollListener.onScrollStateChanged(this, state); + } + if (mScrollListeners != null) { + for (int i = mScrollListeners.size() - 1; i >= 0; i--) { + mScrollListeners.get(i).onScrollStateChanged(this, state); + } + } + } + + /** + * Returns whether there are pending adapter updates which are not yet applied to the layout. + * <p> + * If this method returns <code>true</code>, it means that what user is currently seeing may not + * reflect them adapter contents (depending on what has changed). + * You may use this information to defer or cancel some operations. + * <p> + * This method returns true if RecyclerView has not yet calculated the first layout after it is + * attached to the Window or the Adapter has been replaced. + * + * @return True if there are some adapter updates which are not yet reflected to layout or false + * if layout is up to date. + */ + public boolean hasPendingAdapterUpdates() { + return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout + || mAdapterHelper.hasPendingUpdates(); + } + + class ViewFlinger implements Runnable { + private int mLastFlingX; + private int mLastFlingY; + private OverScroller mScroller; + Interpolator mInterpolator = sQuinticInterpolator; + + + // When set to true, postOnAnimation callbacks are delayed until the run method completes + private boolean mEatRunOnAnimationRequest = false; + + // Tracks if postAnimationCallback should be re-attached when it is done + private boolean mReSchedulePostAnimationCallback = false; + + ViewFlinger() { + mScroller = new OverScroller(getContext(), sQuinticInterpolator); + } + + @Override + public void run() { + if (mLayout == null) { + stop(); + return; // no layout, cannot scroll. + } + disableRunOnAnimationRequests(); + consumePendingUpdateOperations(); + // keep a local reference so that if it is changed during onAnimation method, it won't + // cause unexpected behaviors + final OverScroller scroller = mScroller; + final SmoothScroller smoothScroller = mLayout.mSmoothScroller; + if (scroller.computeScrollOffset()) { + final int x = scroller.getCurrX(); + final int y = scroller.getCurrY(); + final int dx = x - mLastFlingX; + final int dy = y - mLastFlingY; + int hresult = 0; + int vresult = 0; + mLastFlingX = x; + mLastFlingY = y; + int overscrollX = 0, overscrollY = 0; + if (mAdapter != null) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + Trace.beginSection(TRACE_SCROLL_TAG); + if (dx != 0) { + hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); + overscrollX = dx - hresult; + } + if (dy != 0) { + vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState); + overscrollY = dy - vresult; + } + Trace.endSection(); + repositionShadowingViews(); + + onExitLayoutOrScroll(); + resumeRequestLayout(false); + + if (smoothScroller != null && !smoothScroller.isPendingInitialRun() + && smoothScroller.isRunning()) { + final int adapterSize = mState.getItemCount(); + if (adapterSize == 0) { + smoothScroller.stop(); + } else if (smoothScroller.getTargetPosition() >= adapterSize) { + smoothScroller.setTargetPosition(adapterSize - 1); + smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY); + } else { + smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY); + } + } + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + considerReleasingGlowsOnScroll(dx, dy); + } + if (overscrollX != 0 || overscrollY != 0) { + final int vel = (int) scroller.getCurrVelocity(); + + int velX = 0; + if (overscrollX != x) { + velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0; + } + + int velY = 0; + if (overscrollY != y) { + velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0; + } + + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + absorbGlows(velX, velY); + } + if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0) + && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) { + scroller.abortAnimation(); + } + } + if (hresult != 0 || vresult != 0) { + dispatchOnScrolled(hresult, vresult); + } + + if (!awakenScrollBars()) { + invalidate(); + } + + final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically() + && vresult == dy; + final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally() + && hresult == dx; + final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal + || fullyConsumedVertical; + + if (scroller.isFinished() || !fullyConsumedAny) { + setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this. + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } else { + postOnAnimation(); + if (mGapWorker != null) { + mGapWorker.postFromTraversal(RecyclerView.this, dx, dy); + } + } + } + // call this after the onAnimation is complete not to have inconsistent callbacks etc. + if (smoothScroller != null) { + if (smoothScroller.isPendingInitialRun()) { + smoothScroller.onAnimation(0, 0); + } + if (!mReSchedulePostAnimationCallback) { + smoothScroller.stop(); //stop if it does not trigger any scroll + } + } + enableRunOnAnimationRequests(); + } + + private void disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false; + mEatRunOnAnimationRequest = true; + } + + private void enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false; + if (mReSchedulePostAnimationCallback) { + postOnAnimation(); + } + } + + void postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true; + } else { + removeCallbacks(this); + RecyclerView.this.postOnAnimation(this); + } + } + + public void fling(int velocityX, int velocityY) { + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + mScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + postOnAnimation(); + } + + public void smoothScrollBy(int dx, int dy) { + smoothScrollBy(dx, dy, 0, 0); + } + + public void smoothScrollBy(int dx, int dy, int vx, int vy) { + smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy)); + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + private int computeScrollDuration(int dx, int dy, int vx, int vy) { + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final boolean horizontal = absDx > absDy; + final int velocity = (int) Math.sqrt(vx * vx + vy * vy); + final int delta = (int) Math.sqrt(dx * dx + dy * dy); + final int containerSize = horizontal ? getWidth() : getHeight(); + final int halfContainerSize = containerSize / 2; + final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize); + final float distance = halfContainerSize + halfContainerSize + * distanceInfluenceForSnapDuration(distanceRatio); + + final int duration; + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + float absDelta = (float) (horizontal ? absDx : absDy); + duration = (int) (((absDelta / containerSize) + 1) * 300); + } + return Math.min(duration, MAX_SCROLL_DURATION); + } + + public void smoothScrollBy(int dx, int dy, int duration) { + smoothScrollBy(dx, dy, duration, sQuinticInterpolator); + } + + public void smoothScrollBy(int dx, int dy, Interpolator interpolator) { + smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, 0, 0), + interpolator == null ? sQuinticInterpolator : interpolator); + } + + public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { + if (mInterpolator != interpolator) { + mInterpolator = interpolator; + mScroller = new OverScroller(getContext(), interpolator); + } + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + mScroller.startScroll(0, 0, dx, dy, duration); + postOnAnimation(); + } + + public void stop() { + removeCallbacks(this); + mScroller.abortAnimation(); + } + + } + + void repositionShadowingViews() { + // Fix up shadow views used by change animations + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; i++) { + View view = mChildHelper.getChildAt(i); + ViewHolder holder = getChildViewHolder(view); + if (holder != null && holder.mShadowingHolder != null) { + View shadowingView = holder.mShadowingHolder.itemView; + int left = view.getLeft(); + int top = view.getTop(); + if (left != shadowingView.getLeft() || top != shadowingView.getTop()) { + shadowingView.layout(left, top, + left + shadowingView.getWidth(), + top + shadowingView.getHeight()); + } + } + } + } + + private class RecyclerViewDataObserver extends AdapterDataObserver { + RecyclerViewDataObserver() { + } + + @Override + public void onChanged() { + assertNotInLayoutOrScroll(null); + mState.mStructureChanged = true; + + setDataSetChangedAfterLayout(); + if (!mAdapterHelper.hasPendingUpdates()) { + requestLayout(); + } + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) { + triggerUpdateProcessor(); + } + } + + void triggerUpdateProcessor() { + if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { + RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable); + } else { + mAdapterUpdateDuringMeasure = true; + requestLayout(); + } + } + } + + /** + * RecycledViewPool lets you share Views between multiple RecyclerViews. + * <p> + * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool + * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}. + * <p> + * RecyclerView automatically creates a pool for itself if you don't provide one. + * + */ + public static class RecycledViewPool { + private static final int DEFAULT_MAX_SCRAP = 5; + + /** + * Tracks both pooled holders, as well as create/bind timing metadata for the given type. + * + * Note that this tracks running averages of create/bind time across all RecyclerViews + * (and, indirectly, Adapters) that use this pool. + * + * 1) This enables us to track average create and bind times across multiple adapters. Even + * though create (and especially bind) may behave differently for different Adapter + * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type. + * + * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return + * false for all other views of its type for the same deadline. This prevents items + * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch. + */ + static class ScrapData { + ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); + int mMaxScrap = DEFAULT_MAX_SCRAP; + long mCreateRunningAverageNs = 0; + long mBindRunningAverageNs = 0; + } + SparseArray<ScrapData> mScrap = new SparseArray<>(); + + private int mAttachCount = 0; + + public void clear() { + for (int i = 0; i < mScrap.size(); i++) { + ScrapData data = mScrap.valueAt(i); + data.mScrapHeap.clear(); + } + } + + public void setMaxRecycledViews(int viewType, int max) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mMaxScrap = max; + final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; + if (scrapHeap != null) { + while (scrapHeap.size() > max) { + scrapHeap.remove(scrapHeap.size() - 1); + } + } + } + + /** + * Returns the current number of Views held by the RecycledViewPool of the given view type. + */ + public int getRecycledViewCount(int viewType) { + return getScrapDataForType(viewType).mScrapHeap.size(); + } + + public ViewHolder getRecycledView(int viewType) { + final ScrapData scrapData = mScrap.get(viewType); + if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { + final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; + return scrapHeap.remove(scrapHeap.size() - 1); + } + return null; + } + + int size() { + int count = 0; + for (int i = 0; i < mScrap.size(); i++) { + ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap; + if (viewHolders != null) { + count += viewHolders.size(); + } + } + return count; + } + + public void putRecycledView(ViewHolder scrap) { + final int viewType = scrap.getItemViewType(); + final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; + if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { + return; + } + if (DEBUG && scrapHeap.contains(scrap)) { + throw new IllegalArgumentException("this scrap item already exists"); + } + scrap.resetInternal(); + scrapHeap.add(scrap); + } + + long runningAverage(long oldAverage, long newValue) { + if (oldAverage == 0) { + return newValue; + } + return (oldAverage / 4 * 3) + (newValue / 4); + } + + void factorInCreateTime(int viewType, long createTimeNs) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mCreateRunningAverageNs = runningAverage( + scrapData.mCreateRunningAverageNs, createTimeNs); + } + + void factorInBindTime(int viewType, long bindTimeNs) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mBindRunningAverageNs = runningAverage( + scrapData.mBindRunningAverageNs, bindTimeNs); + } + + boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { + long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; + return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); + } + + boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) { + long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs; + return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); + } + + void attach(Adapter adapter) { + mAttachCount++; + } + + void detach() { + mAttachCount--; + } + + + /** + * Detaches the old adapter and attaches the new one. + * <p> + * RecycledViewPool will clear its cache if it has only one adapter attached and the new + * adapter uses a different ViewHolder than the oldAdapter. + * + * @param oldAdapter The previous adapter instance. Will be detached. + * @param newAdapter The new adapter instance. Will be attached. + * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same + * ViewHolder and view types. + */ + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + if (oldAdapter != null) { + detach(); + } + if (!compatibleWithPrevious && mAttachCount == 0) { + clear(); + } + if (newAdapter != null) { + attach(newAdapter); + } + } + + private ScrapData getScrapDataForType(int viewType) { + ScrapData scrapData = mScrap.get(viewType); + if (scrapData == null) { + scrapData = new ScrapData(); + mScrap.put(viewType, scrapData); + } + return scrapData; + } + } + + /** + * Utility method for finding an internal RecyclerView, if present + */ + @Nullable + static RecyclerView findNestedRecyclerView(@NonNull View view) { + if (!(view instanceof ViewGroup)) { + return null; + } + if (view instanceof RecyclerView) { + return (RecyclerView) view; + } + final ViewGroup parent = (ViewGroup) view; + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + final RecyclerView descendant = findNestedRecyclerView(child); + if (descendant != null) { + return descendant; + } + } + return null; + } + + /** + * Utility method for clearing holder's internal RecyclerView, if present + */ + static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) { + if (holder.mNestedRecyclerView != null) { + View item = holder.mNestedRecyclerView.get(); + while (item != null) { + if (item == holder.itemView) { + return; // match found, don't need to clear + } + + ViewParent parent = item.getParent(); + if (parent instanceof View) { + item = (View) parent; + } else { + item = null; + } + } + holder.mNestedRecyclerView = null; // not nested + } + } + + /** + * Time base for deadline-aware work scheduling. Overridable for testing. + * + * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling + * isn't relevant. + */ + long getNanoTime() { + if (ALLOW_THREAD_GAP_WORK) { + return System.nanoTime(); + } else { + return 0; + } + } + + /** + * A Recycler is responsible for managing scrapped or detached item views for reuse. + * + * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but + * that has been marked for removal or reuse.</p> + * + * <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for + * an adapter's data set representing the data at a given position or item ID. + * If the view to be reused is considered "dirty" the adapter will be asked to rebind it. + * If not, the view can be quickly reused by the LayoutManager with no further work. + * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout} + * may be repositioned by a LayoutManager without remeasurement.</p> + */ + public final class Recycler { + final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); + ArrayList<ViewHolder> mChangedScrap = null; + + final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); + + private final List<ViewHolder> + mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); + + private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; + int mViewCacheMax = DEFAULT_CACHE_SIZE; + + RecycledViewPool mRecyclerPool; + + private ViewCacheExtension mViewCacheExtension; + + static final int DEFAULT_CACHE_SIZE = 2; + + /** + * Clear scrap views out of this recycler. Detached views contained within a + * recycled view pool will remain. + */ + public void clear() { + mAttachedScrap.clear(); + recycleAndClearCachedViews(); + } + + /** + * Set the maximum number of detached, valid views we should retain for later use. + * + * @param viewCount Number of views to keep before sending views to the shared pool + */ + public void setViewCacheSize(int viewCount) { + mRequestedCacheMax = viewCount; + updateViewCacheSize(); + } + + void updateViewCacheSize() { + int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0; + mViewCacheMax = mRequestedCacheMax + extraCache; + + // first, try the views that can be recycled + for (int i = mCachedViews.size() - 1; + i >= 0 && mCachedViews.size() > mViewCacheMax; i--) { + recycleCachedViewAt(i); + } + } + + /** + * Returns an unmodifiable list of ViewHolders that are currently in the scrap list. + * + * @return List of ViewHolders in the scrap list. + */ + public List<ViewHolder> getScrapList() { + return mUnmodifiableAttachedScrap; + } + + /** + * Helper method for getViewForPosition. + * <p> + * Checks whether a given view holder can be used for the provided position. + * + * @param holder ViewHolder + * @return true if ViewHolder matches the provided position, false otherwise + */ + boolean validateViewHolderForOffsetPosition(ViewHolder holder) { + // if it is a removed holder, nothing to verify since we cannot ask adapter anymore + // if it is not removed, verify the type and id. + if (holder.isRemoved()) { + if (DEBUG && !mState.isPreLayout()) { + throw new IllegalStateException("should not receive a removed view unless it" + + " is pre layout"); + } + return mState.isPreLayout(); + } + if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + + "adapter position" + holder); + } + if (!mState.isPreLayout()) { + // don't check type if it is pre-layout. + final int type = mAdapter.getItemViewType(holder.mPosition); + if (type != holder.getItemViewType()) { + return false; + } + } + if (mAdapter.hasStableIds()) { + return holder.getItemId() == mAdapter.getItemId(holder.mPosition); + } + return true; + } + + /** + * Attempts to bind view, and account for relevant timing information. If + * deadlineNs != FOREVER_NS, this method may fail to bind, and return false. + * + * @param holder Holder to be bound. + * @param offsetPosition Position of item to be bound. + * @param position Pre-layout position of item to be bound. + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * bind the holder. + * @return + */ + private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition, + int position, long deadlineNs) { + holder.mOwnerRecyclerView = RecyclerView.this; + final int viewType = holder.getItemViewType(); + long startBindNs = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) { + // abort - we have a deadline we can't meet + return false; + } + mAdapter.bindViewHolder(holder, offsetPosition); + long endBindNs = getNanoTime(); + mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs); + attachAccessibilityDelegate(holder.itemView); + if (mState.isPreLayout()) { + holder.mPreLayoutPosition = position; + } + return true; + } + + /** + * Binds the given View to the position. The View can be a View previously retrieved via + * {@link #getViewForPosition(int)} or created by + * {@link Adapter#onCreateViewHolder(ViewGroup, int)}. + * <p> + * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)} + * and let the RecyclerView handle caching. This is a helper method for LayoutManager who + * wants to handle its own recycling logic. + * <p> + * Note that, {@link #getViewForPosition(int)} already binds the View to the position so + * you don't need to call this method unless you want to bind this View to another position. + * + * @param view The view to update. + * @param position The position of the item to bind to this View. + */ + public void bindViewToPosition(View view, int position) { + ViewHolder holder = getChildViewHolderInt(view); + if (holder == null) { + throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot" + + " pass arbitrary views to this method, they should be created by the " + + "Adapter"); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount()); + } + tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS); + + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; + } + + rvLayoutParams.mInsetsDirty = true; + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null; + } + + /** + * RecyclerView provides artificial position range (item count) in pre-layout state and + * automatically maps these positions to {@link Adapter} positions when + * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called. + * <p> + * Usually, LayoutManager does not need to worry about this. However, in some cases, your + * LayoutManager may need to call some custom component with item positions in which + * case you need the actual adapter position instead of the pre layout position. You + * can use this method to convert a pre-layout position to adapter (post layout) position. + * <p> + * Note that if the provided position belongs to a deleted ViewHolder, this method will + * return -1. + * <p> + * Calling this method in post-layout state returns the same value back. + * + * @param position The pre-layout position to convert. Must be greater or equal to 0 and + * less than {@link State#getItemCount()}. + */ + public int convertPreLayoutPositionToPostLayout(int position) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("invalid position " + position + ". State " + + "item count is " + mState.getItemCount()); + } + if (!mState.isPreLayout()) { + return position; + } + return mAdapterHelper.findPositionOffset(position); + } + + /** + * Obtain a view initialized for the given position. + * + * This method should be used by {@link LayoutManager} implementations to obtain + * views to represent data from an {@link Adapter}. + * <p> + * The Recycler may reuse a scrap or detached view from a shared pool if one is + * available for the correct view type. If the adapter has not indicated that the + * data at the given position has changed, the Recycler will attempt to hand back + * a scrap view that was previously initialized for that data without rebinding. + * + * @param position Position to obtain a view for + * @return A view representing the data at <code>position</code> from <code>adapter</code> + */ + public View getViewForPosition(int position) { + return getViewForPosition(position, false); + } + + View getViewForPosition(int position, boolean dryRun) { + return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; + } + + /** + * Attempts to get the ViewHolder for the given position, either from the Recycler scrap, + * cache, the RecycledViewPool, or creating it directly. + * <p> + * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return + * rather than constructing or binding a ViewHolder if it doesn't think it has time. + * If a ViewHolder must be constructed and not enough time remains, null is returned. If a + * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is + * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this. + * + * @param position Position of ViewHolder to be returned. + * @param dryRun True if the ViewHolder should not be removed from scrap/cache/ + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * create/bind the holder if needed. + * + * @return ViewHolder for requested position + */ + @Nullable + ViewHolder tryGetViewHolderForPositionByDeadline(int position, + boolean dryRun, long deadlineNs) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("Invalid item position " + position + + "(" + position + "). Item count:" + mState.getItemCount()); + } + boolean fromScrapOrHiddenOrCache = false; + ViewHolder holder = null; + // 0) If there is a changed scrap, try to find from there + if (mState.isPreLayout()) { + holder = getChangedScrapViewForPosition(position); + fromScrapOrHiddenOrCache = holder != null; + } + // 1) Find by position from scrap/hidden list/cache + if (holder == null) { + holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); + if (holder != null) { + if (!validateViewHolderForOffsetPosition(holder)) { + // recycle holder (and unscrap if relevant) since it can't be used + if (!dryRun) { + // we would like to recycle this but need to make sure it is not used by + // animation logic etc. + holder.addFlags(ViewHolder.FLAG_INVALID); + if (holder.isScrap()) { + removeDetachedView(holder.itemView, false); + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + } + holder = null; + } else { + fromScrapOrHiddenOrCache = true; + } + } + } + if (holder == null) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount()); + } + + final int type = mAdapter.getItemViewType(offsetPosition); + // 2) Find from scrap/cache via stable ids, if exists + if (mAdapter.hasStableIds()) { + holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), + type, dryRun); + if (holder != null) { + // update position + holder.mPosition = offsetPosition; + fromScrapOrHiddenOrCache = true; + } + } + if (holder == null && mViewCacheExtension != null) { + // We are NOT sending the offsetPosition because LayoutManager does not + // know it. + final View view = mViewCacheExtension + .getViewForPositionAndType(this, position, type); + if (view != null) { + holder = getChildViewHolder(view); + if (holder == null) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view which does not have a ViewHolder"); + } else if (holder.shouldIgnore()) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view that is ignored. You must call stopIgnoring before" + + " returning this view."); + } + } + } + if (holder == null) { // fallback to pool + if (DEBUG) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + + position + ") fetching from shared pool"); + } + holder = getRecycledViewPool().getRecycledView(type); + if (holder != null) { + holder.resetInternal(); + if (FORCE_INVALIDATE_DISPLAY_LIST) { + invalidateDisplayListInt(holder); + } + } + } + if (holder == null) { + long start = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { + // abort - we have a deadline we can't meet + return null; + } + holder = mAdapter.createViewHolder(RecyclerView.this, type); + if (ALLOW_THREAD_GAP_WORK) { + // only bother finding nested RV if prefetching + RecyclerView innerView = findNestedRecyclerView(holder.itemView); + if (innerView != null) { + holder.mNestedRecyclerView = new WeakReference<>(innerView); + } + } + + long end = getNanoTime(); + mRecyclerPool.factorInCreateTime(type, end - start); + if (DEBUG) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); + } + } + } + + // This is very ugly but the only place we can grab this information + // before the View is rebound and returned to the LayoutManager for post layout ops. + // We don't need this in pre-layout since the VH is not updated by the LM. + if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder + .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { + holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (mState.mRunSimpleAnimations) { + int changeFlags = ItemAnimator + .buildAdapterChangeFlagsForAnimations(holder); + changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; + final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, + holder, changeFlags, holder.getUnmodifiedPayloads()); + recordAnimationInfoIfBouncedHiddenView(holder, info); + } + } + + boolean bound = false; + if (mState.isPreLayout() && holder.isBound()) { + // do not update unless we absolutely have to. + holder.mPreLayoutPosition = position; + } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { + if (DEBUG && holder.isRemoved()) { + throw new IllegalStateException("Removed holder should be bound and it should" + + " come here only in pre-layout. Holder: " + holder); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); + } + + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; + } + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; + return holder; + } + + private void attachAccessibilityDelegate(View itemView) { + if (isAccessibilityEnabled()) { + if (itemView.getImportantForAccessibility() + == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + if (itemView.getAccessibilityDelegate() == null) { + itemView.setAccessibilityDelegate(mAccessibilityDelegate.getItemDelegate()); + } + } + } + + private void invalidateDisplayListInt(ViewHolder holder) { + if (holder.itemView instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) holder.itemView, false); + } + } + + private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) { + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + final View view = viewGroup.getChildAt(i); + if (view instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) view, true); + } + } + if (!invalidateThis) { + return; + } + // we need to force it to become invisible + if (viewGroup.getVisibility() == View.INVISIBLE) { + viewGroup.setVisibility(View.VISIBLE); + viewGroup.setVisibility(View.INVISIBLE); + } else { + final int visibility = viewGroup.getVisibility(); + viewGroup.setVisibility(View.INVISIBLE); + viewGroup.setVisibility(visibility); + } + } + + /** + * Recycle a detached view. The specified view will be added to a pool of views + * for later rebinding and reuse. + * + * <p>A view must be fully detached (removed from parent) before it may be recycled. If the + * View is scrapped, it will be removed from scrap list.</p> + * + * @param view Removed view for recycling + * @see LayoutManager#removeAndRecycleView(View, Recycler) + */ + public void recycleView(View view) { + // This public recycle method tries to make view recycle-able since layout manager + // intended to recycle this view (e.g. even if it is in scrap or change cache) + ViewHolder holder = getChildViewHolderInt(view); + if (holder.isTmpDetached()) { + removeDetachedView(view, false); + } + if (holder.isScrap()) { + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + } + + /** + * Internally, use this method instead of {@link #recycleView(android.view.View)} to + * catch potential bugs. + * @param view + */ + void recycleViewInternal(View view) { + recycleViewHolderInternal(getChildViewHolderInt(view)); + } + + void recycleAndClearCachedViews() { + final int count = mCachedViews.size(); + for (int i = count - 1; i >= 0; i--) { + recycleCachedViewAt(i); + } + mCachedViews.clear(); + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } + + /** + * Recycles a cached view and removes the view from the list. Views are added to cache + * if and only if they are recyclable, so this method does not check it again. + * <p> + * A small exception to this rule is when the view does not have an animator reference + * but transient state is true (due to animations created outside ItemAnimator). In that + * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is + * still recyclable since Adapter wants to do so. + * + * @param cachedViewIndex The index of the view in cached views list + */ + void recycleCachedViewAt(int cachedViewIndex) { + if (DEBUG) { + Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); + } + ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); + if (DEBUG) { + Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); + } + addViewHolderToRecycledViewPool(viewHolder, true); + mCachedViews.remove(cachedViewIndex); + } + + /** + * internal implementation checks if view is scrapped or attached and throws an exception + * if so. + * Public version un-scraps before calling recycle. + */ + void recycleViewHolderInternal(ViewHolder holder) { + if (holder.isScrap() || holder.itemView.getParent() != null) { + throw new IllegalArgumentException( + "Scrapped or attached views may not be recycled. isScrap:" + + holder.isScrap() + " isAttached:" + + (holder.itemView.getParent() != null)); + } + + if (holder.isTmpDetached()) { + throw new IllegalArgumentException("Tmp detached view should be removed " + + "from RecyclerView before it can be recycled: " + holder); + } + + if (holder.shouldIgnore()) { + throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" + + " should first call stopIgnoringView(view) before calling recycle."); + } + //noinspection unchecked + final boolean transientStatePreventsRecycling = holder + .doesTransientStatePreventRecycling(); + final boolean forceRecycle = mAdapter != null + && transientStatePreventsRecycling + && mAdapter.onFailedToRecycleView(holder); + boolean cached = false; + boolean recycled = false; + if (DEBUG && mCachedViews.contains(holder)) { + throw new IllegalArgumentException("cached view received recycle internal? " + + holder); + } + if (forceRecycle || holder.isRecyclable()) { + if (mViewCacheMax > 0 + && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED + | ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { + // Retire oldest cached view + int cachedViewSize = mCachedViews.size(); + if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { + recycleCachedViewAt(0); + cachedViewSize--; + } + + int targetCacheIndex = cachedViewSize; + if (ALLOW_THREAD_GAP_WORK + && cachedViewSize > 0 + && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { + // when adding the view, skip past most recently prefetched views + int cacheIndex = cachedViewSize - 1; + while (cacheIndex >= 0) { + int cachedPos = mCachedViews.get(cacheIndex).mPosition; + if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { + break; + } + cacheIndex--; + } + targetCacheIndex = cacheIndex + 1; + } + mCachedViews.add(targetCacheIndex, holder); + cached = true; + } + if (!cached) { + addViewHolderToRecycledViewPool(holder, true); + recycled = true; + } + } else { + // NOTE: A view can fail to be recycled when it is scrolled off while an animation + // runs. In this case, the item is eventually recycled by + // ItemAnimatorRestoreListener#onAnimationFinished. + + // TODO: consider cancelling an animation when an item is removed scrollBy, + // to return it to the pool faster + if (DEBUG) { + Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + + "re-visit here. We are still removing it from animation lists"); + } + } + // even if the holder is not removed, we still call this method so that it is removed + // from view holder lists. + mViewInfoStore.removeViewHolder(holder); + if (!cached && !recycled && transientStatePreventsRecycling) { + holder.mOwnerRecyclerView = null; + } + } + + /** + * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool. + * + * Pass false to dispatchRecycled for views that have not been bound. + * + * @param holder Holder to be added to the pool. + * @param dispatchRecycled True to dispatch View recycled callbacks. + */ + void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) { + clearNestedRecyclerViewIfNotNested(holder); + holder.itemView.setAccessibilityDelegate(null); + if (dispatchRecycled) { + dispatchViewRecycled(holder); + } + holder.mOwnerRecyclerView = null; + getRecycledViewPool().putRecycledView(holder); + } + + /** + * Used as a fast path for unscrapping and recycling a view during a bulk operation. + * The caller must call {@link #clearScrap()} when it's done to update the recycler's + * internal bookkeeping. + */ + void quickRecycleScrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + recycleViewHolderInternal(holder); + } + + /** + * Mark an attached view as scrap. + * + * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible + * for rebinding and reuse. Requests for a view for a given position may return a + * reused or rebound scrap view instance.</p> + * + * @param view View to scrap + */ + void scrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) + || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { + if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { + throw new IllegalArgumentException("Called scrap view with an invalid view." + + " Invalid views cannot be reused from scrap, they should rebound from" + + " recycler pool."); + } + holder.setScrapContainer(this, false); + mAttachedScrap.add(holder); + } else { + if (mChangedScrap == null) { + mChangedScrap = new ArrayList<ViewHolder>(); + } + holder.setScrapContainer(this, true); + mChangedScrap.add(holder); + } + } + + /** + * Remove a previously scrapped view from the pool of eligible scrap. + * + * <p>This view will no longer be eligible for reuse until re-scrapped or + * until it is explicitly removed and recycled.</p> + */ + void unscrapView(ViewHolder holder) { + if (holder.mInChangeScrap) { + mChangedScrap.remove(holder); + } else { + mAttachedScrap.remove(holder); + } + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + } + + int getScrapCount() { + return mAttachedScrap.size(); + } + + View getScrapViewAt(int index) { + return mAttachedScrap.get(index).itemView; + } + + void clearScrap() { + mAttachedScrap.clear(); + if (mChangedScrap != null) { + mChangedScrap.clear(); + } + } + + ViewHolder getChangedScrapViewForPosition(int position) { + // If pre-layout, check the changed scrap for an exact match. + final int changedScrapSize; + if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) { + return null; + } + // find by position + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + // find by id + if (mAdapter.hasStableIds()) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) { + final long id = mAdapter.getItemId(offsetPosition); + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + } + } + return null; + } + + /** + * Returns a view for the position either from attach scrap, hidden children, or cache. + * + * @param position Item position + * @param dryRun Does a dry run, finds the ViewHolder but does not remove + * @return a ViewHolder that can be re-used for this position. + */ + ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { + final int scrapCount = mAttachedScrap.size(); + + // Try first for an exact, non-invalid match from scrap. + for (int i = 0; i < scrapCount; i++) { + final ViewHolder holder = mAttachedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position + && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + + if (!dryRun) { + View view = mChildHelper.findHiddenNonRemovedView(position); + if (view != null) { + // This View is good to be used. We just need to unhide, detach and move to the + // scrap list. + final ViewHolder vh = getChildViewHolderInt(view); + mChildHelper.unhide(view); + int layoutIndex = mChildHelper.indexOfChild(view); + if (layoutIndex == RecyclerView.NO_POSITION) { + throw new IllegalStateException("layout index should not be -1 after " + + "unhiding a view:" + vh); + } + mChildHelper.detachViewFromParent(layoutIndex); + scrapView(view); + vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP + | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + return vh; + } + } + + // Search in our first-level recycled view cache. + final int cacheSize = mCachedViews.size(); + for (int i = 0; i < cacheSize; i++) { + final ViewHolder holder = mCachedViews.get(i); + // invalid view holders may be in cache if adapter has stable ids as they can be + // retrieved via getScrapOrCachedViewForId + if (!holder.isInvalid() && holder.getLayoutPosition() == position) { + if (!dryRun) { + mCachedViews.remove(i); + } + if (DEBUG) { + Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position + + ") found match in cache: " + holder); + } + return holder; + } + } + return null; + } + + ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { + // Look in our attached views first + final int count = mAttachedScrap.size(); + for (int i = count - 1; i >= 0; i--) { + final ViewHolder holder = mAttachedScrap.get(i); + if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) { + if (type == holder.getItemViewType()) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + if (holder.isRemoved()) { + // this might be valid in two cases: + // > item is removed but we are in pre-layout pass + // >> do nothing. return as is. make sure we don't rebind + // > item is removed then added to another position and we are in + // post layout. + // >> remove removed and invalid flags, add update flag to rebind + // because item was invisible to us and we don't know what happened in + // between. + if (!mState.isPreLayout()) { + holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED); + } + } + return holder; + } else if (!dryRun) { + // if we are running animations, it is actually better to keep it in scrap + // but this would force layout manager to lay it out which would be bad. + // Recycle this scrap. Type mismatch. + mAttachedScrap.remove(i); + removeDetachedView(holder.itemView, false); + quickRecycleScrapView(holder.itemView); + } + } + } + + // Search the first-level cache + final int cacheSize = mCachedViews.size(); + for (int i = cacheSize - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder.getItemId() == id) { + if (type == holder.getItemViewType()) { + if (!dryRun) { + mCachedViews.remove(i); + } + return holder; + } else if (!dryRun) { + recycleCachedViewAt(i); + return null; + } + } + } + return null; + } + + void dispatchViewRecycled(ViewHolder holder) { + if (mRecyclerListener != null) { + mRecyclerListener.onViewRecycled(holder); + } + if (mAdapter != null) { + mAdapter.onViewRecycled(holder); + } + if (mState != null) { + mViewInfoStore.removeViewHolder(holder); + } + if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder); + } + + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + clear(); + getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious); + } + + void offsetPositionRecordsForMove(int from, int to) { + final int start, end, inBetweenOffset; + if (from < to) { + start = from; + end = to; + inBetweenOffset = -1; + } else { + start = to; + end = from; + inBetweenOffset = 1; + } + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null || holder.mPosition < start || holder.mPosition > end) { + continue; + } + if (holder.mPosition == from) { + holder.offsetPosition(to - from, false); + } else { + holder.offsetPosition(inBetweenOffset, false); + } + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder " + + holder); + } + } + } + + void offsetPositionRecordsForInsert(int insertedAt, int count) { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null && holder.mPosition >= insertedAt) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " + + holder + " now at position " + (holder.mPosition + count)); + } + holder.offsetPosition(count, true); + } + } + } + + /** + * @param removedFrom Remove start index + * @param count Remove count + * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if + * false, they'll be applied before the second layout pass + */ + void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) { + final int removedEnd = removedFrom + count; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + if (holder.mPosition >= removedEnd) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForRemove cached " + i + + " holder " + holder + " now at position " + + (holder.mPosition - count)); + } + holder.offsetPosition(-count, applyToPreLayout); + } else if (holder.mPosition >= removedFrom) { + // Item for this view was removed. Dump it from the cache. + holder.addFlags(ViewHolder.FLAG_REMOVED); + recycleCachedViewAt(i); + } + } + } + } + + void setViewCacheExtension(ViewCacheExtension extension) { + mViewCacheExtension = extension; + } + + void setRecycledViewPool(RecycledViewPool pool) { + if (mRecyclerPool != null) { + mRecyclerPool.detach(); + } + mRecyclerPool = pool; + if (pool != null) { + mRecyclerPool.attach(getAdapter()); + } + } + + RecycledViewPool getRecycledViewPool() { + if (mRecyclerPool == null) { + mRecyclerPool = new RecycledViewPool(); + } + return mRecyclerPool; + } + + void viewRangeUpdate(int positionStart, int itemCount) { + final int positionEnd = positionStart + itemCount; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null) { + continue; + } + + final int pos = holder.getLayoutPosition(); + if (pos >= positionStart && pos < positionEnd) { + holder.addFlags(ViewHolder.FLAG_UPDATE); + recycleCachedViewAt(i); + // cached views should not be flagged as changed because this will cause them + // to animate when they are returned from cache. + } + } + } + + void setAdapterPositionsAsUnknown() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + } + } + } + + void markKnownViewsInvalid() { + if (mAdapter != null && mAdapter.hasStableIds()) { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + holder.addChangePayload(null); + } + } + } else { + // we cannot re-use cached views in this case. Recycle them all + recycleAndClearCachedViews(); + } + } + + void clearOldPositions() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + holder.clearOldPosition(); + } + final int scrapCount = mAttachedScrap.size(); + for (int i = 0; i < scrapCount; i++) { + mAttachedScrap.get(i).clearOldPosition(); + } + if (mChangedScrap != null) { + final int changedScrapCount = mChangedScrap.size(); + for (int i = 0; i < changedScrapCount; i++) { + mChangedScrap.get(i).clearOldPosition(); + } + } + } + + void markItemDecorInsetsDirty() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); + if (layoutParams != null) { + layoutParams.mInsetsDirty = true; + } + } + } + } + + /** + * ViewCacheExtension is a helper class to provide an additional layer of view caching that can + * be controlled by the developer. + * <p> + * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and + * first level cache to find a matching View. If it cannot find a suitable View, Recycler will + * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking + * {@link RecycledViewPool}. + * <p> + * Note that, Recycler never sends Views to this method to be cached. It is developers + * responsibility to decide whether they want to keep their Views in this custom cache or let + * the default recycling policy handle it. + */ + public abstract static class ViewCacheExtension { + + /** + * Returns a View that can be binded to the given Adapter position. + * <p> + * This method should <b>not</b> create a new View. Instead, it is expected to return + * an already created View that can be re-used for the given type and position. + * If the View is marked as ignored, it should first call + * {@link LayoutManager#stopIgnoringView(View)} before returning the View. + * <p> + * RecyclerView will re-bind the returned View to the position if necessary. + * + * @param recycler The Recycler that can be used to bind the View + * @param position The adapter position + * @param type The type of the View, defined by adapter + * @return A View that is bound to the given position or NULL if there is no View to re-use + * @see LayoutManager#ignoreView(View) + */ + public abstract View getViewForPositionAndType(Recycler recycler, int position, int type); + } + + /** + * Base class for an Adapter + * + * <p>Adapters provide a binding from an app-specific data set to views that are displayed + * within a {@link RecyclerView}.</p> + * + * @param <VH> A class that extends ViewHolder that will be used by the adapter. + */ + public abstract static class Adapter<VH extends ViewHolder> { + private final AdapterDataObservable mObservable = new AdapterDataObservable(); + private boolean mHasStableIds = false; + + /** + * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent + * an item. + * <p> + * This new ViewHolder should be constructed with a new View that can represent the items + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + * <p> + * The new ViewHolder will be used to display items of the adapter using + * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param viewType The view type of the new View. + * + * @return A new ViewHolder that holds a View of the given view type. + * @see #getItemViewType(int) + * @see #onBindViewHolder(ViewHolder, int) + */ + public abstract VH onCreateViewHolder(ViewGroup parent, int viewType); + + /** + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given + * position. + * <p> + * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the <code>position</code> parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will + * have the updated adapter position. + * + * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can + * handle efficient partial bind. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + */ + public abstract void onBindViewHolder(VH holder, int position); + + /** + * Called by RecyclerView to display the data at the specified position. This method + * should update the contents of the {@link ViewHolder#itemView} to reflect the item at + * the given position. + * <p> + * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the <code>position</code> parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will + * have the updated adapter position. + * <p> + * Partial bind vs full bind: + * <p> + * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or + * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, + * the ViewHolder is currently bound to old data and Adapter may run an efficient partial + * update using the payload info. If the payload is empty, Adapter must run a full bind. + * Adapter should not assume that the payload passed in notify methods will be received by + * onBindViewHolder(). For example when the view is not attached to the screen, the + * payload in notifyItemChange() will be simply dropped. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + * @param payloads A non-null list of merged payloads. Can be empty list if requires full + * update. + */ + public void onBindViewHolder(VH holder, int position, List<Object> payloads) { + onBindViewHolder(holder, position); + } + + /** + * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new + * {@link ViewHolder} and initializes some private fields to be used by RecyclerView. + * + * @see #onCreateViewHolder(ViewGroup, int) + */ + public final VH createViewHolder(ViewGroup parent, int viewType) { + Trace.beginSection(TRACE_CREATE_VIEW_TAG); + final VH holder = onCreateViewHolder(parent, viewType); + holder.mItemViewType = viewType; + Trace.endSection(); + return holder; + } + + /** + * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the + * {@link ViewHolder} contents with the item at the given position and also sets up some + * private fields to be used by RecyclerView. + * + * @see #onBindViewHolder(ViewHolder, int) + */ + public final void bindViewHolder(VH holder, int position) { + holder.mPosition = position; + if (hasStableIds()) { + holder.mItemId = getItemId(position); + } + holder.setFlags(ViewHolder.FLAG_BOUND, + ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + Trace.beginSection(TRACE_BIND_VIEW_TAG); + onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); + holder.clearPayload(); + final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); + if (layoutParams instanceof RecyclerView.LayoutParams) { + ((LayoutParams) layoutParams).mInsetsDirty = true; + } + Trace.endSection(); + } + + /** + * Return the view type of the item at <code>position</code> for the purposes + * of view recycling. + * + * <p>The default implementation of this method returns 0, making the assumption of + * a single view type for the adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + * + * @param position position to query + * @return integer value identifying the type of the view needed to represent the item at + * <code>position</code>. Type codes need not be contiguous. + */ + public int getItemViewType(int position) { + return 0; + } + + /** + * Indicates whether each item in the data set can be represented with a unique identifier + * of type {@link java.lang.Long}. + * + * @param hasStableIds Whether items in data set have unique identifiers or not. + * @see #hasStableIds() + * @see #getItemId(int) + */ + public void setHasStableIds(boolean hasStableIds) { + if (hasObservers()) { + throw new IllegalStateException("Cannot change whether this adapter has " + + "stable IDs while the adapter has registered observers."); + } + mHasStableIds = hasStableIds; + } + + /** + * Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()} + * would return false this method should return {@link #NO_ID}. The default implementation + * of this method returns {@link #NO_ID}. + * + * @param position Adapter position to query + * @return the stable ID of the item at position + */ + public long getItemId(int position) { + return NO_ID; + } + + /** + * Returns the total number of items in the data set held by the adapter. + * + * @return The total number of items in this adapter. + */ + public abstract int getItemCount(); + + /** + * Returns true if this adapter publishes a unique <code>long</code> value that can + * act as a key for the item at a given position in the data set. If that item is relocated + * in the data set, the ID returned for that item should be the same. + * + * @return true if this adapter's items have stable IDs + */ + public final boolean hasStableIds() { + return mHasStableIds; + } + + /** + * Called when a view created by this adapter has been recycled. + * + * <p>A view is recycled when a {@link LayoutManager} decides that it no longer + * needs to be attached to its parent {@link RecyclerView}. This can be because it has + * fallen out of visibility or a set of cached views represented by views still + * attached to the parent RecyclerView. If an item view has large or expensive data + * bound to it such as large bitmaps, this may be a good place to release those + * resources.</p> + * <p> + * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder for the view being recycled + */ + public void onViewRecycled(VH holder) { + } + + /** + * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled + * due to its transient state. Upon receiving this callback, Adapter can clear the + * animation(s) that effect the View's transient state and return <code>true</code> so that + * the View can be recycled. Keep in mind that the View in question is already removed from + * the RecyclerView. + * <p> + * In some cases, it is acceptable to recycle a View although it has transient state. Most + * of the time, this is a case where the transient state will be cleared in + * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position. + * For this reason, RecyclerView leaves the decision to the Adapter and uses the return + * value of this method to decide whether the View should be recycled or not. + * <p> + * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you + * should never receive this callback because RecyclerView keeps those Views as children + * until their animations are complete. This callback is useful when children of the item + * views create animations which may not be easy to implement using an {@link ItemAnimator}. + * <p> + * You should <em>never</em> fix this issue by calling + * <code>holder.itemView.setHasTransientState(false);</code> unless you've previously called + * <code>holder.itemView.setHasTransientState(true);</code>. Each + * <code>View.setHasTransientState(true)</code> call must be matched by a + * <code>View.setHasTransientState(false)</code> call, otherwise, the state of the View + * may become inconsistent. You should always prefer to end or cancel animations that are + * triggering the transient state instead of handling it manually. + * + * @param holder The ViewHolder containing the View that could not be recycled due to its + * transient state. + * @return True if the View should be recycled, false otherwise. Note that if this method + * returns <code>true</code>, RecyclerView <em>will ignore</em> the transient state of + * the View and recycle it regardless. If this method returns <code>false</code>, + * RecyclerView will check the View's transient state again before giving a final decision. + * Default implementation returns false. + */ + public boolean onFailedToRecycleView(VH holder) { + return false; + } + + /** + * Called when a view created by this adapter has been attached to a window. + * + * <p>This can be used as a reasonable signal that the view is about to be seen + * by the user. If the adapter previously freed any resources in + * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow} + * those resources should be restored here.</p> + * + * @param holder Holder of the view being attached + */ + public void onViewAttachedToWindow(VH holder) { + } + + /** + * Called when a view created by this adapter has been detached from its window. + * + * <p>Becoming detached from the window is not necessarily a permanent condition; + * the consumer of an Adapter's views may choose to cache views offscreen while they + * are not visible, attaching and detaching them as appropriate.</p> + * + * @param holder Holder of the view being detached + */ + public void onViewDetachedFromWindow(VH holder) { + } + + /** + * Returns true if one or more observers are attached to this adapter. + * + * @return true if this adapter has observers + */ + public final boolean hasObservers() { + return mObservable.hasObservers(); + } + + /** + * Register a new observer to listen for data changes. + * + * <p>The adapter may publish a variety of events describing specific changes. + * Not all adapters may support all change types and some may fall back to a generic + * {@link com.android.internal.widget.RecyclerView.AdapterDataObserver#onChanged() + * "something changed"} event if more specific data is not available.</p> + * + * <p>Components registering observers with an adapter are responsible for + * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) + * unregistering} those observers when finished.</p> + * + * @param observer Observer to register + * + * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) + */ + public void registerAdapterDataObserver(AdapterDataObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer currently listening for data changes. + * + * <p>The unregistered observer will no longer receive events about changes + * to the adapter.</p> + * + * @param observer Observer to unregister + * + * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver) + */ + public void unregisterAdapterDataObserver(AdapterDataObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * Called by RecyclerView when it starts observing this Adapter. + * <p> + * Keep in mind that same adapter may be observed by multiple RecyclerViews. + * + * @param recyclerView The RecyclerView instance which started observing this adapter. + * @see #onDetachedFromRecyclerView(RecyclerView) + */ + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + } + + /** + * Called by RecyclerView when it stops observing this Adapter. + * + * @param recyclerView The RecyclerView instance which stopped observing this adapter. + * @see #onAttachedToRecyclerView(RecyclerView) + */ + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + } + + /** + * Notify any registered observers that the data set has changed. + * + * <p>There are two different classes of data change events, item changes and structural + * changes. Item changes are when a single item has its data updated but no positional + * changes have occurred. Structural changes are when items are inserted, removed or moved + * within the data set.</p> + * + * <p>This event does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.</p> + * + * <p><code>RecyclerView</code> will attempt to synthesize visible structural change events + * for adapters that report that they have {@link #hasStableIds() stable IDs} when + * this method is used. This can help for the purposes of animation and visual + * object persistence but individual item views will still need to be rebound + * and relaid out.</p> + * + * <p>If you are writing an adapter it will always be more efficient to use the more + * specific change events if you can. Rely on <code>notifyDataSetChanged()</code> + * as a last resort.</p> + * + * @see #notifyItemChanged(int) + * @see #notifyItemInserted(int) + * @see #notifyItemRemoved(int) + * @see #notifyItemRangeChanged(int, int) + * @see #notifyItemRangeInserted(int, int) + * @see #notifyItemRangeRemoved(int, int) + */ + public final void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Notify any registered observers that the item at <code>position</code> has changed. + * Equivalent to calling <code>notifyItemChanged(position, null);</code>. + * + * <p>This is an item change event, not a structural change event. It indicates that any + * reflection of the data at <code>position</code> is out of date and should be updated. + * The item at <code>position</code> retains the same identity.</p> + * + * @param position Position of the item that has changed + * + * @see #notifyItemRangeChanged(int, int) + */ + public final void notifyItemChanged(int position) { + mObservable.notifyItemRangeChanged(position, 1); + } + + /** + * Notify any registered observers that the item at <code>position</code> has changed with + * an optional payload object. + * + * <p>This is an item change event, not a structural change event. It indicates that any + * reflection of the data at <code>position</code> is out of date and should be updated. + * The item at <code>position</code> retains the same identity. + * </p> + * + * <p> + * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param position Position of the item that has changed + * @param payload Optional parameter, use null to identify a "full" update + * + * @see #notifyItemRangeChanged(int, int) + */ + public final void notifyItemChanged(int position, Object payload) { + mObservable.notifyItemRangeChanged(position, 1, payload); + } + + /** + * Notify any registered observers that the <code>itemCount</code> items starting at + * position <code>positionStart</code> have changed. + * Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>. + * + * <p>This is an item change event, not a structural change event. It indicates that + * any reflection of the data in the given position range is out of date and should + * be updated. The items in the given range retain the same identity.</p> + * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * + * @see #notifyItemChanged(int) + */ + public final void notifyItemRangeChanged(int positionStart, int itemCount) { + mObservable.notifyItemRangeChanged(positionStart, itemCount); + } + + /** + * Notify any registered observers that the <code>itemCount</code> items starting at + * position <code>positionStart</code> have changed. An optional payload can be + * passed to each changed item. + * + * <p>This is an item change event, not a structural change event. It indicates that any + * reflection of the data in the given position range is out of date and should be updated. + * The items in the given range retain the same identity. + * </p> + * + * <p> + * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * @param payload Optional parameter, use null to identify a "full" update + * + * @see #notifyItemChanged(int) + */ + public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { + mObservable.notifyItemRangeChanged(positionStart, itemCount, payload); + } + + /** + * Notify any registered observers that the item reflected at <code>position</code> + * has been newly inserted. The item previously at <code>position</code> is now at + * position <code>position + 1</code>. + * + * <p>This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.</p> + * + * @param position Position of the newly inserted item in the data set + * + * @see #notifyItemRangeInserted(int, int) + */ + public final void notifyItemInserted(int position) { + mObservable.notifyItemRangeInserted(position, 1); + } + + /** + * Notify any registered observers that the item reflected at <code>fromPosition</code> + * has been moved to <code>toPosition</code>. + * + * <p>This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.</p> + * + * @param fromPosition Previous position of the item. + * @param toPosition New position of the item. + */ + public final void notifyItemMoved(int fromPosition, int toPosition) { + mObservable.notifyItemMoved(fromPosition, toPosition); + } + + /** + * Notify any registered observers that the currently reflected <code>itemCount</code> + * items starting at <code>positionStart</code> have been newly inserted. The items + * previously located at <code>positionStart</code> and beyond can now be found starting + * at position <code>positionStart + itemCount</code>. + * + * <p>This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.</p> + * + * @param positionStart Position of the first item that was inserted + * @param itemCount Number of items inserted + * + * @see #notifyItemInserted(int) + */ + public final void notifyItemRangeInserted(int positionStart, int itemCount) { + mObservable.notifyItemRangeInserted(positionStart, itemCount); + } + + /** + * Notify any registered observers that the item previously located at <code>position</code> + * has been removed from the data set. The items previously located at and after + * <code>position</code> may now be found at <code>oldPosition - 1</code>. + * + * <p>This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.</p> + * + * @param position Position of the item that has now been removed + * + * @see #notifyItemRangeRemoved(int, int) + */ + public final void notifyItemRemoved(int position) { + mObservable.notifyItemRangeRemoved(position, 1); + } + + /** + * Notify any registered observers that the <code>itemCount</code> items previously + * located at <code>positionStart</code> have been removed from the data set. The items + * previously located at and after <code>positionStart + itemCount</code> may now be found + * at <code>oldPosition - itemCount</code>. + * + * <p>This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.</p> + * + * @param positionStart Previous position of the first item that was removed + * @param itemCount Number of items removed from the data set + */ + public final void notifyItemRangeRemoved(int positionStart, int itemCount) { + mObservable.notifyItemRangeRemoved(positionStart, itemCount); + } + } + + void dispatchChildDetached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildDetachedFromWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewDetachedFromWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); + } + } + } + + void dispatchChildAttached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildAttachedToWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewAttachedToWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); + } + } + } + + /** + * A <code>LayoutManager</code> is responsible for measuring and positioning item views + * within a <code>RecyclerView</code> as well as determining the policy for when to recycle + * item views that are no longer visible to the user. By changing the <code>LayoutManager</code> + * a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list, + * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock + * layout managers are provided for general use. + * <p/> + * If the LayoutManager specifies a default constructor or one with the signature + * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will + * instantiate and set the LayoutManager when being inflated. Most used properties can + * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case + * a LayoutManager specifies both constructors, the non-default constructor will take + * precedence. + * + */ + public abstract static class LayoutManager { + ChildHelper mChildHelper; + RecyclerView mRecyclerView; + + @Nullable + SmoothScroller mSmoothScroller; + + boolean mRequestedSimpleAnimations = false; + + boolean mIsAttachedToWindow = false; + + boolean mAutoMeasure = false; + + /** + * LayoutManager has its own more strict measurement cache to avoid re-measuring a child + * if the space that will be given to it is already larger than what it has measured before. + */ + private boolean mMeasurementCacheEnabled = true; + + private boolean mItemPrefetchEnabled = true; + + /** + * Written by {@link GapWorker} when prefetches occur to track largest number of view ever + * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or + * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call. + * + * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, + * will be reset upon layout to prevent initial prefetches (often large, since they're + * proportional to expected child count) from expanding cache permanently. + */ + int mPrefetchMaxCountObserved; + + /** + * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset. + */ + boolean mPrefetchMaxObservedInInitialPrefetch; + + /** + * These measure specs might be the measure specs that were passed into RecyclerView's + * onMeasure method OR fake measure specs created by the RecyclerView. + * For example, when a layout is run, RecyclerView always sets these specs to be + * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass. + * <p> + * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the + * API level and sets the size to 0 pre-M to avoid any issue that might be caused by + * corrupt values. Older platforms have no responsibility to provide a size if they set + * mode to unspecified. + */ + private int mWidthMode, mHeightMode; + private int mWidth, mHeight; + + + /** + * Interface for LayoutManagers to request items to be prefetched, based on position, with + * specified distance from viewport, which indicates priority. + * + * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public interface LayoutPrefetchRegistry { + /** + * Requests an an item to be prefetched, based on position, with a specified distance, + * indicating priority. + * + * @param layoutPosition Position of the item to prefetch. + * @param pixelDistance Distance from the current viewport to the bounds of the item, + * must be non-negative. + */ + void addPosition(int layoutPosition, int pixelDistance); + } + + void setRecyclerView(RecyclerView recyclerView) { + if (recyclerView == null) { + mRecyclerView = null; + mChildHelper = null; + mWidth = 0; + mHeight = 0; + } else { + mRecyclerView = recyclerView; + mChildHelper = recyclerView.mChildHelper; + mWidth = recyclerView.getWidth(); + mHeight = recyclerView.getHeight(); + } + mWidthMode = MeasureSpec.EXACTLY; + mHeightMode = MeasureSpec.EXACTLY; + } + + void setMeasureSpecs(int wSpec, int hSpec) { + mWidth = MeasureSpec.getSize(wSpec); + mWidthMode = MeasureSpec.getMode(wSpec); + if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mWidth = 0; + } + + mHeight = MeasureSpec.getSize(hSpec); + mHeightMode = MeasureSpec.getMode(hSpec); + if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mHeight = 0; + } + } + + /** + * Called after a layout is calculated during a measure pass when using auto-measure. + * <p> + * It simply traverses all children to calculate a bounding box then calls + * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method + * if they need to handle the bounding box differently. + * <p> + * For example, GridLayoutManager override that method to ensure that even if a column is + * empty, the GridLayoutManager still measures wide enough to include it. + * + * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure + * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure + */ + void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { + final int count = getChildCount(); + if (count == 0) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + return; + } + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(child, bounds); + if (bounds.left < minX) { + minX = bounds.left; + } + if (bounds.right > maxX) { + maxX = bounds.right; + } + if (bounds.top < minY) { + minY = bounds.top; + } + if (bounds.bottom > maxY) { + maxY = bounds.bottom; + } + } + mRecyclerView.mTempRect.set(minX, minY, maxX, maxY); + setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); + } + + /** + * Sets the measured dimensions from the given bounding box of the children and the + * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is + * called after the RecyclerView calls + * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a measurement pass. + * <p> + * This method should call {@link #setMeasuredDimension(int, int)}. + * <p> + * The default implementation adds the RecyclerView's padding to the given bounding box + * then caps the value to be within the given measurement specs. + * <p> + * This method is only called if the LayoutManager opted into the auto measurement API. + * + * @param childrenBounds The bounding box of all children + * @param wSpec The widthMeasureSpec that was passed into the RecyclerView. + * @param hSpec The heightMeasureSpec that was passed into the RecyclerView. + * + * @see #setAutoMeasureEnabled(boolean) + */ + public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { + int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight(); + int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom(); + int width = chooseSize(wSpec, usedWidth, getMinimumWidth()); + int height = chooseSize(hSpec, usedHeight, getMinimumHeight()); + setMeasuredDimension(width, height); + } + + /** + * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView + */ + public void requestLayout() { + if (mRecyclerView != null) { + mRecyclerView.requestLayout(); + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it <b>is not</b>. + * + * @param message The message for the exception. Can be null. + * @see #assertNotInLayoutOrScroll(String) + */ + public void assertInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertInLayoutOrScroll(message); + } + } + + /** + * Chooses a size from the given specs and parameters that is closest to the desired size + * and also complies with the spec. + * + * @param spec The measureSpec + * @param desired The preferred measurement + * @param min The minimum value + * + * @return A size that fits to the given specs + */ + public static int chooseSize(int spec, int desired, int min) { + final int mode = View.MeasureSpec.getMode(spec); + final int size = View.MeasureSpec.getSize(spec); + switch (mode) { + case View.MeasureSpec.EXACTLY: + return size; + case View.MeasureSpec.AT_MOST: + return Math.min(size, Math.max(desired, min)); + case View.MeasureSpec.UNSPECIFIED: + default: + return Math.max(desired, min); + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it <b>is</b>. + * + * @param message The message for the exception. Can be null. + * @see #assertInLayoutOrScroll(String) + */ + public void assertNotInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertNotInLayoutOrScroll(message); + } + } + + /** + * Defines whether the layout should be measured by the RecyclerView or the LayoutManager + * wants to handle the layout measurements itself. + * <p> + * This method is usually called by the LayoutManager with value {@code true} if it wants + * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize + * the measurement logic, you can call this method with {@code false} and override + * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic. + * <p> + * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or + * handle various specs provided by the RecyclerView's parent. + * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an + * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based + * on children's positions. It does this while supporting all existing animation + * capabilities of the RecyclerView. + * <p> + * AutoMeasure works as follows: + * <ol> + * <li>LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of + * the framework LayoutManagers use {@code auto-measure}.</li> + * <li>When {@link RecyclerView#onMeasure(int, int)} is called, if the provided specs are + * exact, RecyclerView will only call LayoutManager's {@code onMeasure} and return without + * doing any layout calculation.</li> + * <li>If one of the layout specs is not {@code EXACT}, the RecyclerView will start the + * layout process in {@code onMeasure} call. It will process all pending Adapter updates and + * decide whether to run a predictive layout or not. If it decides to do so, it will first + * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to + * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still + * return the width and height of the RecyclerView as of the last layout calculation. + * <p> + * After handling the predictive case, RecyclerView will call + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can + * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()}, + * {@link #getWidth()} and {@link #getWidthMode()}.</li> + * <li>After the layout calculation, RecyclerView sets the measured width & height by + * calculating the bounding box for the children (+ RecyclerView's padding). The + * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose + * different values. For instance, GridLayoutManager overrides this value to handle the case + * where if it is vertical and has 3 columns but only 2 items, it should still measure its + * width to fit 3 items, not 2.</li> + * <li>Any following on measure call to the RecyclerView will run + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will + * take care of which views are actually added / removed / moved / changed for animations so + * that the LayoutManager should not worry about them and handle each + * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one. + * </li> + * <li>When measure is complete and RecyclerView's + * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks + * whether it already did layout calculations during the measure pass and if so, it re-uses + * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)} + * if the last measure spec was different from the final dimensions or adapter contents + * have changed between the measure call and the layout call.</li> + * <li>Finally, animations are calculated and run as usual.</li> + * </ol> + * + * @param enabled <code>True</code> if the Layout should be measured by the + * RecyclerView, <code>false</code> if the LayoutManager wants + * to measure itself. + * + * @see #setMeasuredDimension(Rect, int, int) + * @see #isAutoMeasureEnabled() + */ + public void setAutoMeasureEnabled(boolean enabled) { + mAutoMeasure = enabled; + } + + /** + * Returns whether the LayoutManager uses the automatic measurement API or not. + * + * @return <code>True</code> if the LayoutManager is measured by the RecyclerView or + * <code>false</code> if it measures itself. + * + * @see #setAutoMeasureEnabled(boolean) + */ + public boolean isAutoMeasureEnabled() { + return mAutoMeasure; + } + + /** + * Returns whether this LayoutManager supports automatic item animations. + * A LayoutManager wishing to support item animations should obey certain + * rules as outlined in {@link #onLayoutChildren(Recycler, State)}. + * The default return value is <code>false</code>, so subclasses of LayoutManager + * will not get predictive item animations by default. + * + * <p>Whether item animations are enabled in a RecyclerView is determined both + * by the return value from this method and the + * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the + * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this + * method returns false, then simple item animations will be enabled, in which + * views that are moving onto or off of the screen are simply faded in/out. If + * the RecyclerView has a non-null ItemAnimator and this method returns true, + * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to + * setup up the information needed to more intelligently predict where appearing + * and disappearing views should be animated from/to.</p> + * + * @return true if predictive item animations should be enabled, false otherwise + */ + public boolean supportsPredictiveItemAnimations() { + return false; + } + + /** + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. + * + * <p>If enabled, the LayoutManager will be queried for items to inflate/bind in between + * view system traversals on devices running API 21 or greater. Default value is true.</p> + * + * <p>On platforms API level 21 and higher, the UI thread is idle between passing a frame + * to RenderThread and the starting up its next frame at the next VSync pulse. By + * prefetching out of window views in this time period, delays from inflation and view + * binding are much less likely to cause jank and stuttering during scrolls and flings.</p> + * + * <p>While prefetch is enabled, it will have the side effect of expanding the effective + * size of the View cache to hold prefetched views.</p> + * + * @param enabled <code>True</code> if items should be prefetched in between traversals. + * + * @see #isItemPrefetchEnabled() + */ + public final void setItemPrefetchEnabled(boolean enabled) { + if (enabled != mItemPrefetchEnabled) { + mItemPrefetchEnabled = enabled; + mPrefetchMaxCountObserved = 0; + if (mRecyclerView != null) { + mRecyclerView.mRecycler.updateViewCacheSize(); + } + } + } + + /** + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. + * + * @see #setItemPrefetchEnabled(boolean) + * + * @return true if item prefetch is enabled, false otherwise + */ + public final boolean isItemPrefetchEnabled() { + return mItemPrefetchEnabled; + } + + /** + * Gather all positions from the LayoutManager to be prefetched, given specified momentum. + * + * <p>If item prefetch is enabled, this method is called in between traversals to gather + * which positions the LayoutManager will soon need, given upcoming movement in subsequent + * traversals.</p> + * + * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.</p> + * + * @param dx X movement component. + * @param dy Y movement component. + * @param state State of RecyclerView + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * + * @see #isItemPrefetchEnabled() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public void collectAdjacentPrefetchPositions(int dx, int dy, State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) {} + + /** + * Gather all positions from the LayoutManager to be prefetched in preperation for its + * RecyclerView to come on screen, due to the movement of another, containing RecyclerView. + * + * <p>This method is only called when a RecyclerView is nested in another RecyclerView.</p> + * + * <p>If item prefetch is enabled for this LayoutManager, as well in another containing + * LayoutManager, this method is called in between draw traversals to gather + * which positions this LayoutManager will first need, once it appears on the screen.</p> + * + * <p>For example, if this LayoutManager represents a horizontally scrolling list within a + * vertically scrolling LayoutManager, this method would be called when the horizontal list + * is about to come onscreen.</p> + * + * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.</p> + * + * @param adapterItemCount number of items in the associated adapter. + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * + * @see #isItemPrefetchEnabled() + * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + */ + public void collectInitialPrefetchPositions(int adapterItemCount, + LayoutPrefetchRegistry layoutPrefetchRegistry) {} + + void dispatchAttachedToWindow(RecyclerView view) { + mIsAttachedToWindow = true; + onAttachedToWindow(view); + } + + void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) { + mIsAttachedToWindow = false; + onDetachedFromWindow(view, recycler); + } + + /** + * Returns whether LayoutManager is currently attached to a RecyclerView which is attached + * to a window. + * + * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView + * is attached to window. + */ + public boolean isAttachedToWindow() { + return mIsAttachedToWindow; + } + + /** + * Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread. + * <p> + * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * + * @param action The Runnable that will be executed. + * + * @see #removeCallbacks + */ + public void postOnAnimation(Runnable action) { + if (mRecyclerView != null) { + mRecyclerView.postOnAnimation(action); + } + } + + /** + * Removes the specified Runnable from the message queue. + * <p> + * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * + * @param action The Runnable to remove from the message handling queue + * + * @return true if RecyclerView could ask the Handler to remove the Runnable, + * false otherwise. When the returned value is true, the Runnable + * may or may not have been actually removed from the message queue + * (for instance, if the Runnable was not in the queue already.) + * + * @see #postOnAnimation + */ + public boolean removeCallbacks(Runnable action) { + if (mRecyclerView != null) { + return mRecyclerView.removeCallbacks(action); + } + return false; + } + /** + * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView + * is attached to a window. + * <p> + * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. + * <p> + * Subclass implementations should always call through to the superclass implementation. + * + * @param view The RecyclerView this LayoutManager is bound to + * + * @see #onDetachedFromWindow(RecyclerView, Recycler) + */ + @CallSuper + public void onAttachedToWindow(RecyclerView view) { + } + + /** + * @deprecated + * override {@link #onDetachedFromWindow(RecyclerView, Recycler)} + */ + @Deprecated + public void onDetachedFromWindow(RecyclerView view) { + + } + + /** + * Called when this LayoutManager is detached from its parent RecyclerView or when + * its parent RecyclerView is detached from its window. + * <p> + * LayoutManager should clear all of its View references as another LayoutManager might be + * assigned to the RecyclerView. + * <p> + * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. + * <p> + * If your LayoutManager has View references that it cleans in on-detach, it should also + * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when + * RecyclerView is re-attached. + * <p> + * Subclass implementations should always call through to the superclass implementation. + * + * @param view The RecyclerView this LayoutManager is bound to + * @param recycler The recycler to use if you prefer to recycle your children instead of + * keeping them around. + * + * @see #onAttachedToWindow(RecyclerView) + */ + @CallSuper + public void onDetachedFromWindow(RecyclerView view, Recycler recycler) { + onDetachedFromWindow(view); + } + + /** + * Check if the RecyclerView is configured to clip child views to its padding. + * + * @return true if this RecyclerView clips children to its padding, false otherwise + */ + public boolean getClipToPadding() { + return mRecyclerView != null && mRecyclerView.mClipToPadding; + } + + /** + * Lay out all relevant child views from the given adapter. + * + * The LayoutManager is in charge of the behavior of item animations. By default, + * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple + * item animations are enabled. This means that add/remove operations on the + * adapter will result in animations to add new or appearing items, removed or + * disappearing items, and moved items. If a LayoutManager returns false from + * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a + * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the + * RecyclerView will have enough information to run those animations in a simple + * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will + * simply fade views in and out, whether they are actually added/removed or whether + * they are moved on or off the screen due to other add/remove operations. + * + * <p>A LayoutManager wanting a better item animation experience, where items can be + * animated onto and off of the screen according to where the items exist when they + * are not on screen, then the LayoutManager should return true from + * {@link #supportsPredictiveItemAnimations()} and add additional logic to + * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations + * means that {@link #onLayoutChildren(Recycler, State)} will be called twice; + * once as a "pre" layout step to determine where items would have been prior to + * a real layout, and again to do the "real" layout. In the pre-layout phase, + * items will remember their pre-layout positions to allow them to be laid out + * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will + * be returned from the scrap to help determine correct placement of other items. + * These removed items should not be added to the child list, but should be used + * to help calculate correct positioning of other views, including views that + * were not previously onscreen (referred to as APPEARING views), but whose + * pre-layout offscreen position can be determined given the extra + * information about the pre-layout removed views.</p> + * + * <p>The second layout pass is the real layout in which only non-removed views + * will be used. The only additional requirement during this pass is, if + * {@link #supportsPredictiveItemAnimations()} returns true, to note which + * views exist in the child list prior to layout and which are not there after + * layout (referred to as DISAPPEARING views), and to position/layout those views + * appropriately, without regard to the actual bounds of the RecyclerView. This allows + * the animation system to know the location to which to animate these disappearing + * views.</p> + * + * <p>The default LayoutManager implementations for RecyclerView handle all of these + * requirements for animations already. Clients of RecyclerView can either use one + * of these layout managers directly or look at their implementations of + * onLayoutChildren() to see how they account for the APPEARING and + * DISAPPEARING views.</p> + * + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + */ + public void onLayoutChildren(Recycler recycler, State state) { + Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) "); + } + + /** + * Called after a full layout calculation is finished. The layout calculation may include + * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or + * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call. + * This method will be called at the end of {@link View#layout(int, int, int, int)} call. + * <p> + * This is a good place for the LayoutManager to do some cleanup like pending scroll + * position, saved state etc. + * + * @param state Transient state of RecyclerView + */ + public void onLayoutCompleted(State state) { + } + + /** + * Create a default <code>LayoutParams</code> object for a child of the RecyclerView. + * + * <p>LayoutManagers will often want to use a custom <code>LayoutParams</code> type + * to store extra information specific to the layout. Client code should subclass + * {@link RecyclerView.LayoutParams} for this purpose.</p> + * + * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p> + * + * @return A new LayoutParams for a child view + */ + public abstract LayoutParams generateDefaultLayoutParams(); + + /** + * Determines the validity of the supplied LayoutParams object. + * + * <p>This should check to make sure that the object is of the correct type + * and all values are within acceptable ranges. The default implementation + * returns <code>true</code> for non-null params.</p> + * + * @param lp LayoutParams object to check + * @return true if this LayoutParams object is valid, false otherwise + */ + public boolean checkLayoutParams(LayoutParams lp) { + return lp != null; + } + + /** + * Create a LayoutParams object suitable for this LayoutManager, copying relevant + * values from the supplied LayoutParams object if possible. + * + * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p> + * + * @param lp Source LayoutParams object to copy values from + * @return a new LayoutParams object + */ + public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof LayoutParams) { + return new LayoutParams((LayoutParams) lp); + } else if (lp instanceof MarginLayoutParams) { + return new LayoutParams((MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + /** + * Create a LayoutParams object suitable for this LayoutManager from + * an inflated layout resource. + * + * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p> + * + * @param c Context for obtaining styled attributes + * @param attrs AttributeSet describing the supplied arguments + * @return a new LayoutParams object + */ + public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + /** + * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. + * + * @param dx distance to scroll by in pixels. X increases as scroll position + * approaches the right. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dx was + * negative and scrolling proceeeded in that direction. + * <code>Math.abs(result)</code> may be less than dx if a boundary was reached. + */ + public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { + return 0; + } + + /** + * Scroll vertically by dy pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. + * + * @param dy distance to scroll in pixels. Y increases as scroll position + * approaches the bottom. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dy was + * negative and scrolling proceeeded in that direction. + * <code>Math.abs(result)</code> may be less than dy if a boundary was reached. + */ + public int scrollVerticallyBy(int dy, Recycler recycler, State state) { + return 0; + } + + /** + * Query if horizontal scrolling is currently supported. The default implementation + * returns false. + * + * @return True if this LayoutManager can scroll the current contents horizontally + */ + public boolean canScrollHorizontally() { + return false; + } + + /** + * Query if vertical scrolling is currently supported. The default implementation + * returns false. + * + * @return True if this LayoutManager can scroll the current contents vertically + */ + public boolean canScrollVertically() { + return false; + } + + /** + * Scroll to the specified adapter position. + * + * Actual position of the item on the screen depends on the LayoutManager implementation. + * @param position Scroll to this adapter position. + */ + public void scrollToPosition(int position) { + if (DEBUG) { + Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract"); + } + } + + /** + * <p>Smooth scroll to the specified adapter position.</p> + * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller} + * instance and call {@link #startSmoothScroll(SmoothScroller)}. + * </p> + * @param recyclerView The RecyclerView to which this layout manager is attached + * @param state Current State of RecyclerView + * @param position Scroll to this adapter position. + */ + public void smoothScrollToPosition(RecyclerView recyclerView, State state, + int position) { + Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling"); + } + + /** + * <p>Starts a smooth scroll using the provided SmoothScroller.</p> + * <p>Calling this method will cancel any previous smooth scroll request.</p> + * @param smoothScroller Instance which defines how smooth scroll should be animated + */ + public void startSmoothScroll(SmoothScroller smoothScroller) { + if (mSmoothScroller != null && smoothScroller != mSmoothScroller + && mSmoothScroller.isRunning()) { + mSmoothScroller.stop(); + } + mSmoothScroller = smoothScroller; + mSmoothScroller.start(mRecyclerView, this); + } + + /** + * @return true if RecycylerView is currently in the state of smooth scrolling. + */ + public boolean isSmoothScrolling() { + return mSmoothScroller != null && mSmoothScroller.isRunning(); + } + + + /** + * Returns the resolved layout direction for this RecyclerView. + * + * @return {@link android.view.View#LAYOUT_DIRECTION_RTL} if the layout + * direction is RTL or returns + * {@link android.view.View#LAYOUT_DIRECTION_LTR} if the layout direction + * is not RTL. + */ + public int getLayoutDirection() { + return mRecyclerView.getLayoutDirection(); + } + + /** + * Ends all animations on the view created by the {@link ItemAnimator}. + * + * @param view The View for which the animations should be ended. + * @see RecyclerView.ItemAnimator#endAnimations() + */ + public void endAnimation(View view) { + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view)); + } + } + + /** + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + * <p> + * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. + * + * @param child View to add and then remove with animation. + */ + public void addDisappearingView(View child) { + addDisappearingView(child, -1); + } + + /** + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + * <p> + * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. + * + * @param child View to add and then remove with animation. + * @param index Index of the view. + */ + public void addDisappearingView(View child, int index) { + addViewInt(child, index, true); + } + + /** + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. + * + * @param child View to add + */ + public void addView(View child) { + addView(child, -1); + } + + /** + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. + * + * @param child View to add + * @param index Index to add child at + */ + public void addView(View child, int index) { + addViewInt(child, index, false); + } + + private void addViewInt(View child, int index, boolean disappearing) { + final ViewHolder holder = getChildViewHolderInt(child); + if (disappearing || holder.isRemoved()) { + // these views will be hidden at the end of the layout pass. + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder); + } else { + // This may look like unnecessary but may happen if layout manager supports + // predictive layouts and adapter removed then re-added the same item. + // In this case, added version will be visible in the post layout (because add is + // deferred) but RV will still bind it to the same View. + // So if a View re-appears in post layout pass, remove it from disappearing list. + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder); + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (holder.wasReturnedFromScrap() || holder.isScrap()) { + if (holder.isScrap()) { + holder.unScrap(); + } else { + holder.clearReturnedFromScrapFlag(); + } + mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); + if (DISPATCH_TEMP_DETACH) { + child.dispatchFinishTemporaryDetach(); + } + } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child + // ensure in correct position + int currentIndex = mChildHelper.indexOfChild(child); + if (index == -1) { + index = mChildHelper.getChildCount(); + } + if (currentIndex == -1) { + throw new IllegalStateException("Added View has RecyclerView as parent but" + + " view is not a real child. Unfiltered index:" + + mRecyclerView.indexOfChild(child)); + } + if (currentIndex != index) { + mRecyclerView.mLayout.moveView(currentIndex, index); + } + } else { + mChildHelper.addView(child, index, false); + lp.mInsetsDirty = true; + if (mSmoothScroller != null && mSmoothScroller.isRunning()) { + mSmoothScroller.onChildAttachedToWindow(child); + } + } + if (lp.mPendingInvalidate) { + if (DEBUG) { + Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder); + } + holder.itemView.invalidate(); + lp.mPendingInvalidate = false; + } + } + + /** + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. + * + * @param child View to remove + */ + public void removeView(View child) { + mChildHelper.removeView(child); + } + + /** + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. + * + * @param index Index of the child view to remove + */ + public void removeViewAt(int index) { + final View child = getChildAt(index); + if (child != null) { + mChildHelper.removeViewAt(index); + } + } + + /** + * Remove all views from the currently attached RecyclerView. This will not recycle + * any of the affected views; the LayoutManager is responsible for doing so if desired. + */ + public void removeAllViews() { + // Only remove non-animating views + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + mChildHelper.removeViewAt(i); + } + } + + /** + * Returns offset of the RecyclerView's text baseline from the its top boundary. + * + * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if + * there is no baseline. + */ + public int getBaseline() { + return -1; + } + + /** + * Returns the adapter position of the item represented by the given View. This does not + * contain any adapter changes that might have happened after the last layout. + * + * @param view The view to query + * @return The adapter position of the item which is rendered by this View. + */ + public int getPosition(View view) { + return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); + } + + /** + * Returns the View type defined by the adapter. + * + * @param view The view to query + * @return The type of the view assigned by the adapter. + */ + public int getItemViewType(View view) { + return getChildViewHolderInt(view).getItemViewType(); + } + + /** + * Traverses the ancestors of the given view and returns the item view that contains it + * and also a direct child of the LayoutManager. + * <p> + * Note that this method may return null if the view is a child of the RecyclerView but + * not a child of the LayoutManager (e.g. running a disappear animation). + * + * @param view The view that is a descendant of the LayoutManager. + * + * @return The direct child of the LayoutManager which contains the given view or null if + * the provided view is not a descendant of this LayoutManager. + * + * @see RecyclerView#getChildViewHolder(View) + * @see RecyclerView#findContainingViewHolder(View) + */ + @Nullable + public View findContainingItemView(View view) { + if (mRecyclerView == null) { + return null; + } + View found = mRecyclerView.findContainingItemView(view); + if (found == null) { + return null; + } + if (mChildHelper.isHidden(found)) { + return null; + } + return found; + } + + /** + * Finds the view which represents the given adapter position. + * <p> + * This method traverses each child since it has no information about child order. + * Override this method to improve performance if your LayoutManager keeps data about + * child views. + * <p> + * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method. + * + * @param position Position of the item in adapter + * @return The child view that represents the given position or null if the position is not + * laid out + */ + public View findViewByPosition(int position) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + ViewHolder vh = getChildViewHolderInt(child); + if (vh == null) { + continue; + } + if (vh.getLayoutPosition() == position && !vh.shouldIgnore() + && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) { + return child; + } + } + return null; + } + + /** + * Temporarily detach a child view. + * + * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.</p> + * + * <p>If a LayoutManager uses this method to detach a view, it <em>must</em> + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.</p> + * + * @param child Child to detach + */ + public void detachView(View child) { + final int ind = mChildHelper.indexOfChild(child); + if (ind >= 0) { + detachViewInternal(ind, child); + } + } + + /** + * Temporarily detach a child view. + * + * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.</p> + * + * <p>If a LayoutManager uses this method to detach a view, it <em>must</em> + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.</p> + * + * @param index Index of the child to detach + */ + public void detachViewAt(int index) { + detachViewInternal(index, getChildAt(index)); + } + + private void detachViewInternal(int index, View view) { + if (DISPATCH_TEMP_DETACH) { + view.dispatchStartTemporaryDetach(); + } + mChildHelper.detachViewFromParent(index); + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + * @param lp LayoutParams for child + */ + public void attachView(View child, int index, LayoutParams lp) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh.isRemoved()) { + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh); + } else { + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh); + } + mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved()); + if (DISPATCH_TEMP_DETACH) { + child.dispatchFinishTemporaryDetach(); + } + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + */ + public void attachView(View child, int index) { + attachView(child, index, (LayoutParams) child.getLayoutParams()); + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + */ + public void attachView(View child) { + attachView(child, -1); + } + + /** + * Finish removing a view that was previously temporarily + * {@link #detachView(android.view.View) detached}. + * + * @param child Detached child to remove + */ + public void removeDetachedView(View child) { + mRecyclerView.removeDetachedView(child, false); + } + + /** + * Moves a View from one position to another. + * + * @param fromIndex The View's initial index + * @param toIndex The View's target index + */ + public void moveView(int fromIndex, int toIndex) { + View view = getChildAt(fromIndex); + if (view == null) { + throw new IllegalArgumentException("Cannot move a child from non-existing index:" + + fromIndex); + } + detachViewAt(fromIndex); + attachView(view, toIndex); + } + + /** + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + * <p>Scrapping a view allows it to be rebound and reused to show updated or + * different data.</p> + * + * @param child Child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into + */ + public void detachAndScrapView(View child, Recycler recycler) { + int index = mChildHelper.indexOfChild(child); + scrapOrRecycleView(recycler, index, child); + } + + /** + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + * <p>Scrapping a view allows it to be rebound and reused to show updated or + * different data.</p> + * + * @param index Index of child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into + */ + public void detachAndScrapViewAt(int index, Recycler recycler) { + final View child = getChildAt(index); + scrapOrRecycleView(recycler, index, child); + } + + /** + * Remove a child view and recycle it using the given Recycler. + * + * @param child Child to remove and recycle + * @param recycler Recycler to use to recycle child + */ + public void removeAndRecycleView(View child, Recycler recycler) { + removeView(child); + recycler.recycleView(child); + } + + /** + * Remove a child view and recycle it using the given Recycler. + * + * @param index Index of child to remove and recycle + * @param recycler Recycler to use to recycle child + */ + public void removeAndRecycleViewAt(int index, Recycler recycler) { + final View view = getChildAt(index); + removeViewAt(index); + recycler.recycleView(view); + } + + /** + * Return the current number of child views attached to the parent RecyclerView. + * This does not include child views that were temporarily detached and/or scrapped. + * + * @return Number of attached children + */ + public int getChildCount() { + return mChildHelper != null ? mChildHelper.getChildCount() : 0; + } + + /** + * Return the child view at the given index + * @param index Index of child to return + * @return Child view at index + */ + public View getChildAt(int index) { + return mChildHelper != null ? mChildHelper.getChildAt(index) : null; + } + + /** + * Return the width measurement spec mode of the RecyclerView. + * <p> + * This value is set only if the LayoutManager opts into the auto measure api via + * {@link #setAutoMeasureEnabled(boolean)}. + * <p> + * When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * + * @return Width measure spec mode. + * + * @see View.MeasureSpec#getMode(int) + * @see View#onMeasure(int, int) + */ + public int getWidthMode() { + return mWidthMode; + } + + /** + * Return the height measurement spec mode of the RecyclerView. + * <p> + * This value is set only if the LayoutManager opts into the auto measure api via + * {@link #setAutoMeasureEnabled(boolean)}. + * <p> + * When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * + * @return Height measure spec mode. + * + * @see View.MeasureSpec#getMode(int) + * @see View#onMeasure(int, int) + */ + public int getHeightMode() { + return mHeightMode; + } + + /** + * Return the width of the parent RecyclerView + * + * @return Width in pixels + */ + public int getWidth() { + return mWidth; + } + + /** + * Return the height of the parent RecyclerView + * + * @return Height in pixels + */ + public int getHeight() { + return mHeight; + } + + /** + * Return the left padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingLeft() { + return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0; + } + + /** + * Return the top padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingTop() { + return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0; + } + + /** + * Return the right padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingRight() { + return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0; + } + + /** + * Return the bottom padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingBottom() { + return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0; + } + + /** + * Return the start padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingStart() { + return mRecyclerView != null ? mRecyclerView.getPaddingStart() : 0; + } + + /** + * Return the end padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingEnd() { + return mRecyclerView != null ? mRecyclerView.getPaddingEnd() : 0; + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has focus. + * + * @return True if the RecyclerView has focus, false otherwise. + * @see View#isFocused() + */ + public boolean isFocused() { + return mRecyclerView != null && mRecyclerView.isFocused(); + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus. + * + * @return true if the RecyclerView has or contains focus + * @see View#hasFocus() + */ + public boolean hasFocus() { + return mRecyclerView != null && mRecyclerView.hasFocus(); + } + + /** + * Returns the item View which has or contains focus. + * + * @return A direct child of RecyclerView which has focus or contains the focused child. + */ + public View getFocusedChild() { + if (mRecyclerView == null) { + return null; + } + final View focused = mRecyclerView.getFocusedChild(); + if (focused == null || mChildHelper.isHidden(focused)) { + return null; + } + return focused; + } + + /** + * Returns the number of items in the adapter bound to the parent RecyclerView. + * <p> + * Note that this number is not necessarily equal to + * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is + * available, you should use {@link State#getItemCount() State#getItemCount()} instead. + * For more details, check the documentation for + * {@link State#getItemCount() State#getItemCount()}. + * + * @return The number of items in the bound adapter + * @see State#getItemCount() + */ + public int getItemCount() { + final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null; + return a != null ? a.getItemCount() : 0; + } + + /** + * Offset all child views attached to the parent RecyclerView by dx pixels along + * the horizontal axis. + * + * @param dx Pixels to offset by + */ + public void offsetChildrenHorizontal(int dx) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenHorizontal(dx); + } + } + + /** + * Offset all child views attached to the parent RecyclerView by dy pixels along + * the vertical axis. + * + * @param dy Pixels to offset by + */ + public void offsetChildrenVertical(int dy) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenVertical(dy); + } + } + + /** + * Flags a view so that it will not be scrapped or recycled. + * <p> + * Scope of ignoring a child is strictly restricted to position tracking, scrapping and + * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child + * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not + * ignore the child. + * <p> + * Before this child can be recycled again, you have to call + * {@link #stopIgnoringView(View)}. + * <p> + * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * + * @param view View to ignore. + * @see #stopIgnoringView(View) + */ + public void ignoreView(View view) { + if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) { + // checking this because calling this method on a recycled or detached view may + // cause loss of state. + throw new IllegalArgumentException("View should be fully attached to be ignored"); + } + final ViewHolder vh = getChildViewHolderInt(view); + vh.addFlags(ViewHolder.FLAG_IGNORE); + mRecyclerView.mViewInfoStore.removeViewHolder(vh); + } + + /** + * View can be scrapped and recycled again. + * <p> + * Note that calling this method removes all information in the view holder. + * <p> + * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * + * @param view View to ignore. + */ + public void stopIgnoringView(View view) { + final ViewHolder vh = getChildViewHolderInt(view); + vh.stopIgnoring(); + vh.resetInternal(); + vh.addFlags(ViewHolder.FLAG_INVALID); + } + + /** + * Temporarily detach and scrap all currently attached child views. Views will be scrapped + * into the given Recycler. The Recycler may prefer to reuse scrap views before + * other views that were previously recycled. + * + * @param recycler Recycler to scrap views into + */ + public void detachAndScrapAttachedViews(Recycler recycler) { + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View v = getChildAt(i); + scrapOrRecycleView(recycler, i, v); + } + } + + private void scrapOrRecycleView(Recycler recycler, int index, View view) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + if (viewHolder.shouldIgnore()) { + if (DEBUG) { + Log.d(TAG, "ignoring view " + viewHolder); + } + return; + } + if (viewHolder.isInvalid() && !viewHolder.isRemoved() + && !mRecyclerView.mAdapter.hasStableIds()) { + removeViewAt(index); + recycler.recycleViewHolderInternal(viewHolder); + } else { + detachViewAt(index); + recycler.scrapView(view); + mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); + } + } + + /** + * Recycles the scrapped views. + * <p> + * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is + * the expected behavior if scrapped views are used for animations. Otherwise, we need to + * call remove and invalidate RecyclerView to ensure UI update. + * + * @param recycler Recycler + */ + void removeAndRecycleScrapInt(Recycler recycler) { + final int scrapCount = recycler.getScrapCount(); + // Loop backward, recycler might be changed by removeDetachedView() + for (int i = scrapCount - 1; i >= 0; i--) { + final View scrap = recycler.getScrapViewAt(i); + final ViewHolder vh = getChildViewHolderInt(scrap); + if (vh.shouldIgnore()) { + continue; + } + // If the scrap view is animating, we need to cancel them first. If we cancel it + // here, ItemAnimator callback may recycle it which will cause double recycling. + // To avoid this, we mark it as not recycleable before calling the item animator. + // Since removeDetachedView calls a user API, a common mistake (ending animations on + // the view) may recycle it too, so we guard it before we call user APIs. + vh.setIsRecyclable(false); + if (vh.isTmpDetached()) { + mRecyclerView.removeDetachedView(scrap, false); + } + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(vh); + } + vh.setIsRecyclable(true); + recycler.quickRecycleScrapView(scrap); + } + recycler.clearScrap(); + if (scrapCount > 0) { + mRecyclerView.invalidate(); + } + } + + + /** + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView and any added item decorations into account. + * + * <p>If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p> + * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant + */ + public void measureChild(View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } + } + + /** + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. + * <p> + * Use this method if the View is already measured once in this layout pass. + */ + boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height); + } + + // we may consider making this public + /** + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. + * <p> + * Use this method if the View is not yet measured and you need to decide whether to + * measure this View or not. + */ + boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return child.isLayoutRequested() + || !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height); + } + + /** + * In addition to the View Framework's measurement cache, RecyclerView uses its own + * additional measurement cache for its children to avoid re-measuring them when not + * necessary. It is on by default but it can be turned off via + * {@link #setMeasurementCacheEnabled(boolean)}. + * + * @return True if measurement cache is enabled, false otherwise. + * + * @see #setMeasurementCacheEnabled(boolean) + */ + public boolean isMeasurementCacheEnabled() { + return mMeasurementCacheEnabled; + } + + /** + * Sets whether RecyclerView should use its own measurement cache for the children. This is + * a more aggressive cache than the framework uses. + * + * @param measurementCacheEnabled True to enable the measurement cache, false otherwise. + * + * @see #isMeasurementCacheEnabled() + */ + public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) { + mMeasurementCacheEnabled = measurementCacheEnabled; + } + + private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) { + final int specMode = MeasureSpec.getMode(spec); + final int specSize = MeasureSpec.getSize(spec); + if (dimension > 0 && childSize != dimension) { + return false; + } + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + return true; + case MeasureSpec.AT_MOST: + return specSize >= childSize; + case MeasureSpec.EXACTLY: + return specSize == childSize; + } + return false; + } + + /** + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView, any added item decorations and the child margins + * into account. + * + * <p>If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p> + * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant + */ + public void measureChildWithMargins(View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } + } + + /** + * Calculate a MeasureSpec value for measuring a child view in one dimension. + * + * @param parentSize Size of the parent view where the child will be placed + * @param padding Total space currently consumed by other elements of the parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * + * @return a MeasureSpec value for the child view + * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)} + */ + @Deprecated + public static int getChildMeasureSpec(int parentSize, int padding, int childDimension, + boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else { + // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap + // instead using UNSPECIFIED. + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + // TODO this should be my spec. + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + /** + * Calculate a MeasureSpec value for measuring a child view in one dimension. + * + * @param parentSize Size of the parent view where the child will be placed + * @param parentMode The measurement spec mode of the parent + * @param padding Total space currently consumed by other elements of parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * + * @return a MeasureSpec value for the child view + */ + public static int getChildMeasureSpec(int parentSize, int parentMode, int padding, + int childDimension, boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + switch (parentMode) { + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + resultSize = size; + resultMode = parentMode; + break; + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + resultMode = parentMode; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) { + resultMode = MeasureSpec.AT_MOST; + } else { + resultMode = MeasureSpec.UNSPECIFIED; + } + + } + } + //noinspection WrongConstant + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + /** + * Returns the measured width of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. + * + * @param child Child view to query + * @return child's measured width plus <code>ItemDecoration</code> insets + * + * @see View#getMeasuredWidth() + */ + public int getDecoratedMeasuredWidth(View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredWidth() + insets.left + insets.right; + } + + /** + * Returns the measured height of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. + * + * @param child Child view to query + * @return child's measured height plus <code>ItemDecoration</code> insets + * + * @see View#getMeasuredHeight() + */ + public int getDecoratedMeasuredHeight(View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredHeight() + insets.top + insets.bottom; + } + + /** + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations}. + * + * <p>LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:</p> + * <ul> + * <li>{@link #layoutDecoratedWithMargins(View, int, int, int, int)}</li> + * <li>{@link #getDecoratedBoundsWithMargins(View, Rect)}</li> + * <li>{@link #measureChild(View, int, int)}</li> + * <li>{@link #measureChildWithMargins(View, int, int)}</li> + * <li>{@link #getDecoratedLeft(View)}</li> + * <li>{@link #getDecoratedTop(View)}</li> + * <li>{@link #getDecoratedRight(View)}</li> + * <li>{@link #getDecoratedBottom(View)}</li> + * <li>{@link #getDecoratedMeasuredWidth(View)}</li> + * <li>{@link #getDecoratedMeasuredHeight(View)}</li> + * </ul> + * + * @param child Child to lay out + * @param left Left edge, with item decoration insets included + * @param top Top edge, with item decoration insets included + * @param right Right edge, with item decoration insets included + * @param bottom Bottom edge, with item decoration insets included + * + * @see View#layout(int, int, int, int) + * @see #layoutDecoratedWithMargins(View, int, int, int, int) + */ + public void layoutDecorated(View child, int left, int top, int right, int bottom) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + child.layout(left + insets.left, top + insets.top, right - insets.right, + bottom - insets.bottom); + } + + /** + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations} and margins. + * + * <p>LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:</p> + * <ul> + * <li>{@link #layoutDecorated(View, int, int, int, int)}</li> + * <li>{@link #measureChild(View, int, int)}</li> + * <li>{@link #measureChildWithMargins(View, int, int)}</li> + * <li>{@link #getDecoratedLeft(View)}</li> + * <li>{@link #getDecoratedTop(View)}</li> + * <li>{@link #getDecoratedRight(View)}</li> + * <li>{@link #getDecoratedBottom(View)}</li> + * <li>{@link #getDecoratedMeasuredWidth(View)}</li> + * <li>{@link #getDecoratedMeasuredHeight(View)}</li> + * </ul> + * + * @param child Child to lay out + * @param left Left edge, with item decoration insets and left margin included + * @param top Top edge, with item decoration insets and top margin included + * @param right Right edge, with item decoration insets and right margin included + * @param bottom Bottom edge, with item decoration insets and bottom margin included + * + * @see View#layout(int, int, int, int) + * @see #layoutDecorated(View, int, int, int, int) + */ + public void layoutDecoratedWithMargins(View child, int left, int top, int right, + int bottom) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, + right - insets.right - lp.rightMargin, + bottom - insets.bottom - lp.bottomMargin); + } + + /** + * Calculates the bounding box of the View while taking into account its matrix changes + * (translation, scale etc) with respect to the RecyclerView. + * <p> + * If {@code includeDecorInsets} is {@code true}, they are applied first before applying + * the View's matrix so that the decor offsets also go through the same transformation. + * + * @param child The ItemView whose bounding box should be calculated. + * @param includeDecorInsets True if the decor insets should be included in the bounding box + * @param out The rectangle into which the output will be written. + */ + public void getTransformedBoundingBox(View child, boolean includeDecorInsets, Rect out) { + if (includeDecorInsets) { + Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + out.set(-insets.left, -insets.top, + child.getWidth() + insets.right, child.getHeight() + insets.bottom); + } else { + out.set(0, 0, child.getWidth(), child.getHeight()); + } + + if (mRecyclerView != null) { + final Matrix childMatrix = child.getMatrix(); + if (childMatrix != null && !childMatrix.isIdentity()) { + final RectF tempRectF = mRecyclerView.mTempRectF; + tempRectF.set(out); + childMatrix.mapRect(tempRectF); + out.set( + (int) Math.floor(tempRectF.left), + (int) Math.floor(tempRectF.top), + (int) Math.ceil(tempRectF.right), + (int) Math.ceil(tempRectF.bottom) + ); + } + } + out.offset(child.getLeft(), child.getTop()); + } + + /** + * Returns the bounds of the view including its decoration and margins. + * + * @param view The view element to check + * @param outBounds A rect that will receive the bounds of the element including its + * decoration and margins. + */ + public void getDecoratedBoundsWithMargins(View view, Rect outBounds) { + RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds); + } + + /** + * Returns the left edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child left edge with offsets applied + * @see #getLeftDecorationWidth(View) + */ + public int getDecoratedLeft(View child) { + return child.getLeft() - getLeftDecorationWidth(child); + } + + /** + * Returns the top edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child top edge with offsets applied + * @see #getTopDecorationHeight(View) + */ + public int getDecoratedTop(View child) { + return child.getTop() - getTopDecorationHeight(child); + } + + /** + * Returns the right edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child right edge with offsets applied + * @see #getRightDecorationWidth(View) + */ + public int getDecoratedRight(View child) { + return child.getRight() + getRightDecorationWidth(child); + } + + /** + * Returns the bottom edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child bottom edge with offsets applied + * @see #getBottomDecorationHeight(View) + */ + public int getDecoratedBottom(View child) { + return child.getBottom() + getBottomDecorationHeight(child); + } + + /** + * Calculates the item decor insets applied to the given child and updates the provided + * Rect instance with the inset values. + * <ul> + * <li>The Rect's left is set to the total width of left decorations.</li> + * <li>The Rect's top is set to the total height of top decorations.</li> + * <li>The Rect's right is set to the total width of right decorations.</li> + * <li>The Rect's bottom is set to total height of bottom decorations.</li> + * </ul> + * <p> + * Note that item decorations are automatically calculated when one of the LayoutManager's + * measure child methods is called. If you need to measure the child with custom specs via + * {@link View#measure(int, int)}, you can use this method to get decorations. + * + * @param child The child view whose decorations should be calculated + * @param outRect The Rect to hold result values + */ + public void calculateItemDecorationsForChild(View child, Rect outRect) { + if (mRecyclerView == null) { + outRect.set(0, 0, 0, 0); + return; + } + Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + outRect.set(insets); + } + + /** + * Returns the total height of item decorations applied to child's top. + * <p> + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's top. + * @see #getDecoratedTop(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getTopDecorationHeight(View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top; + } + + /** + * Returns the total height of item decorations applied to child's bottom. + * <p> + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's bottom. + * @see #getDecoratedBottom(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getBottomDecorationHeight(View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom; + } + + /** + * Returns the total width of item decorations applied to child's left. + * <p> + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's left. + * @see #getDecoratedLeft(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getLeftDecorationWidth(View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left; + } + + /** + * Returns the total width of item decorations applied to child's right. + * <p> + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's right. + * @see #getDecoratedRight(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getRightDecorationWidth(View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right; + } + + /** + * Called when searching for a focusable view in the given direction has failed + * for the current content of the RecyclerView. + * + * <p>This is the LayoutManager's opportunity to populate views in the given direction + * to fulfill the request if it can. The LayoutManager should attach and return + * the view to be focused. The default implementation returns null.</p> + * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @param recycler The recycler to use for obtaining views for currently offscreen items + * @param state Transient state of RecyclerView + * @return The chosen view to be focused + */ + @Nullable + public View onFocusSearchFailed(View focused, int direction, Recycler recycler, + State state) { + return null; + } + + /** + * This method gives a LayoutManager an opportunity to intercept the initial focus search + * before the default behavior of {@link FocusFinder} is used. If this method returns + * null FocusFinder will attempt to find a focusable child view. If it fails + * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} + * will be called to give the LayoutManager an opportunity to add new views for items + * that did not have attached views representing them. The LayoutManager should not add + * or remove views from this method. + * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @return A descendant view to focus or null to fall back to default behavior. + * The default implementation returns null. + */ + public View onInterceptFocusSearch(View focused, int direction) { + return null; + } + + /** + * Called when a child of the RecyclerView wants a particular rectangle to be positioned + * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, + * android.graphics.Rect, boolean)} for more details. + * + * <p>The base implementation will attempt to perform a standard programmatic scroll + * to bring the given rect into view, within the padded area of the RecyclerView.</p> + * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, + boolean immediate) { + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final int childLeft = child.getLeft() + rect.left - child.getScrollX(); + final int childTop = child.getTop() + rect.top - child.getScrollY(); + final int childRight = childLeft + rect.width(); + final int childBottom = childTop + rect.height(); + + final int offScreenLeft = Math.min(0, childLeft - parentLeft); + final int offScreenTop = Math.min(0, childTop - parentTop); + final int offScreenRight = Math.max(0, childRight - parentRight); + final int offScreenBottom = Math.max(0, childBottom - parentBottom); + + // Favor the "start" layout direction over the end when bringing one side or the other + // of a large rect into view. If we decide to bring in end because start is already + // visible, limit the scroll such that start won't go out of bounds. + final int dx; + if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + dx = offScreenRight != 0 ? offScreenRight + : Math.max(offScreenLeft, childRight - parentRight); + } else { + dx = offScreenLeft != 0 ? offScreenLeft + : Math.min(childLeft - parentLeft, offScreenRight); + } + + // Favor bringing the top into view over the bottom. If top is already visible and + // we should scroll to make bottom visible, make sure top does not go out of bounds. + final int dy = offScreenTop != 0 ? offScreenTop + : Math.min(childTop - parentTop, offScreenBottom); + + if (dx != 0 || dy != 0) { + if (immediate) { + parent.scrollBy(dx, dy); + } else { + parent.smoothScrollBy(dx, dy); + } + return true; + } + return false; + } + + /** + * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} + */ + @Deprecated + public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { + // eat the request if we are in the middle of a scroll or layout + return isSmoothScrolling() || parent.isComputingLayout(); + } + + /** + * Called when a descendant view of the RecyclerView requests focus. + * + * <p>A LayoutManager wishing to keep focused views aligned in a specific + * portion of the view may implement that behavior in an override of this method.</p> + * + * <p>If the LayoutManager executes different behavior that should override the default + * behavior of scrolling the focused child on screen instead of running alongside it, + * this method should return true.</p> + * + * @param parent The RecyclerView hosting this LayoutManager + * @param state Current state of RecyclerView + * @param child Direct child of the RecyclerView containing the newly focused view + * @param focused The newly focused view. This may be the same view as child or it may be + * null + * @return true if the default scroll behavior should be suppressed + */ + public boolean onRequestChildFocus(RecyclerView parent, State state, View child, + View focused) { + return onRequestChildFocus(parent, child, focused); + } + + /** + * Called if the RecyclerView this LayoutManager is bound to has a different adapter set. + * The LayoutManager may use this opportunity to clear caches and configure state such + * that it can relayout appropriately with the new data and potentially new view types. + * + * <p>The default implementation removes all currently attached views.</p> + * + * @param oldAdapter The previous adapter instance. Will be null if there was previously no + * adapter. + * @param newAdapter The new adapter instance. Might be null if + * {@link #setAdapter(RecyclerView.Adapter)} is called with {@code null}. + */ + public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) { + } + + /** + * Called to populate focusable views within the RecyclerView. + * + * <p>The LayoutManager implementation should return <code>true</code> if the default + * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be + * suppressed.</p> + * + * <p>The default implementation returns <code>false</code> to trigger RecyclerView + * to fall back to the default ViewGroup behavior.</p> + * + * @param recyclerView The RecyclerView hosting this LayoutManager + * @param views List of output views. This method should add valid focusable views + * to this list. + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @param focusableMode The type of focusables to be added. + * + * @return true to suppress the default behavior, false to add default focusables after + * this method returns. + * + * @see #FOCUSABLES_ALL + * @see #FOCUSABLES_TOUCH_MODE + */ + public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views, + int direction, int focusableMode) { + return false; + } + + /** + * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving + * detailed information on what has actually changed. + * + * @param recyclerView + */ + public void onItemsChanged(RecyclerView recyclerView) { + } + + /** + * Called when items have been added to the adapter. The LayoutManager may choose to + * requestLayout if the inserted items would require refreshing the currently visible set + * of child views. (e.g. currently empty space would be filled by appended items, etc.) + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { + } + + /** + * Called when items have been removed from the adapter. + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { + } + + /** + * Called when items have been changed in the adapter. + * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)} + * instead, then this callback will not be invoked. + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { + } + + /** + * Called when items have been changed in the adapter and with optional payload. + * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}. + * + * @param recyclerView + * @param positionStart + * @param itemCount + * @param payload + */ + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + Object payload) { + onItemsUpdated(recyclerView, positionStart, itemCount); + } + + /** + * Called when an item is moved withing the adapter. + * <p> + * Note that, an item may also change position in response to another ADD/REMOVE/MOVE + * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved} + * is called. + * + * @param recyclerView + * @param from + * @param to + * @param itemCount + */ + public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { + + } + + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current state of RecyclerView + * @return The horizontal extent of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollExtent() + */ + public int computeHorizontalScrollExtent(State state) { + return 0; + } + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current State of RecyclerView where you can find total item count + * @return The horizontal offset of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollOffset() + */ + public int computeHorizontalScrollOffset(State state) { + return 0; + } + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeHorizontalScrollRange()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current State of RecyclerView where you can find total item count + * @return The total horizontal range represented by the vertical scrollbar + * @see RecyclerView#computeHorizontalScrollRange() + */ + public int computeHorizontalScrollRange(State state) { + return 0; + } + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeVerticalScrollExtent()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current state of RecyclerView + * @return The vertical extent of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollExtent() + */ + public int computeVerticalScrollExtent(State state) { + return 0; + } + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeVerticalScrollOffset()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current State of RecyclerView where you can find total item count + * @return The vertical offset of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollOffset() + */ + public int computeVerticalScrollOffset(State state) { + return 0; + } + + /** + * <p>Override this method if you want to support scroll bars.</p> + * + * <p>Read {@link RecyclerView#computeVerticalScrollRange()} for details.</p> + * + * <p>Default implementation returns 0.</p> + * + * @param state Current State of RecyclerView where you can find total item count + * @return The total vertical range represented by the vertical scrollbar + * @see RecyclerView#computeVerticalScrollRange() + */ + public int computeVerticalScrollRange(State state) { + return 0; + } + + /** + * Measure the attached RecyclerView. Implementations must call + * {@link #setMeasuredDimension(int, int)} before returning. + * + * <p>The default implementation will handle EXACTLY measurements and respect + * the minimum width and height properties of the host RecyclerView if measured + * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView + * will consume all available space.</p> + * + * @param recycler Recycler + * @param state Transient state of RecyclerView + * @param widthSpec Width {@link android.view.View.MeasureSpec} + * @param heightSpec Height {@link android.view.View.MeasureSpec} + */ + public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + } + + /** + * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the + * host RecyclerView. + * + * @param widthSize Measured width + * @param heightSize Measured height + */ + public void setMeasuredDimension(int widthSize, int heightSize) { + mRecyclerView.setMeasuredDimension(widthSize, heightSize); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumWidth()} + */ + public int getMinimumWidth() { + return mRecyclerView.getMinimumWidth(); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumHeight()} + */ + public int getMinimumHeight() { + return mRecyclerView.getMinimumHeight(); + } + /** + * <p>Called when the LayoutManager should save its state. This is a good time to save your + * scroll position, configuration and anything else that may be required to restore the same + * layout state if the LayoutManager is recreated.</p> + * <p>RecyclerView does NOT verify if the LayoutManager has changed between state save and + * restore. This will let you share information between your LayoutManagers but it is also + * your responsibility to make sure they use the same parcelable class.</p> + * + * @return Necessary information for LayoutManager to be able to restore its state + */ + public Parcelable onSaveInstanceState() { + return null; + } + + + public void onRestoreInstanceState(Parcelable state) { + + } + + void stopSmoothScroller() { + if (mSmoothScroller != null) { + mSmoothScroller.stop(); + } + } + + private void onSmoothScrollerStopped(SmoothScroller smoothScroller) { + if (mSmoothScroller == smoothScroller) { + mSmoothScroller = null; + } + } + + /** + * RecyclerView calls this method to notify LayoutManager that scroll state has changed. + * + * @param state The new scroll state for RecyclerView + */ + public void onScrollStateChanged(int state) { + } + + /** + * Removes all views and recycles them using the given recycler. + * <p> + * If you want to clean cached views as well, you should call {@link Recycler#clear()} too. + * <p> + * If a View is marked as "ignored", it is not removed nor recycled. + * + * @param recycler Recycler to use to recycle children + * @see #removeAndRecycleView(View, Recycler) + * @see #removeAndRecycleViewAt(int, Recycler) + * @see #ignoreView(View) + */ + public void removeAndRecycleAllViews(Recycler recycler) { + for (int i = getChildCount() - 1; i >= 0; i--) { + final View view = getChildAt(i); + if (!getChildViewHolderInt(view).shouldIgnore()) { + removeAndRecycleViewAt(i, recycler); + } + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info); + } + + /** + * Called by the AccessibilityDelegate when the information about the current layout should + * be populated. + * <p> + * Default implementation adds a {@link + * android.view.accessibility.AccessibilityNodeInfo.CollectionInfo}. + * <p> + * You should override + * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and + * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for + * more accurate accessibility information. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param info The info that should be filled by the LayoutManager + * @see View#onInitializeAccessibilityNodeInfo( + *android.view.accessibility.AccessibilityNodeInfo) + * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) + * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) + */ + public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state, + AccessibilityNodeInfo info) { + if (mRecyclerView.canScrollVertically(-1) + || mRecyclerView.canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); + } + if (mRecyclerView.canScrollVertically(1) + || mRecyclerView.canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.setScrollable(true); + } + final AccessibilityNodeInfo.CollectionInfo collectionInfo = + AccessibilityNodeInfo.CollectionInfo + .obtain(getRowCountForAccessibility(recycler, state), + getColumnCountForAccessibility(recycler, state), + isLayoutHierarchical(recycler, state), + getSelectionModeForAccessibility(recycler, state)); + info.setCollectionInfo(collectionInfo); + } + + // called by accessibility delegate + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event); + } + + /** + * Called by the accessibility delegate to initialize an accessibility event. + * <p> + * Default implementation adds item count and scroll information to the event. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param event The event instance to initialize + * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(Recycler recycler, State state, + AccessibilityEvent event) { + if (mRecyclerView == null || event == null) { + return; + } + event.setScrollable(mRecyclerView.canScrollVertically(1) + || mRecyclerView.canScrollVertically(-1) + || mRecyclerView.canScrollHorizontally(-1) + || mRecyclerView.canScrollHorizontally(1)); + + if (mRecyclerView.mAdapter != null) { + event.setItemCount(mRecyclerView.mAdapter.getItemCount()); + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfo info) { + final ViewHolder vh = getChildViewHolderInt(host); + // avoid trying to create accessibility node info for removed children + if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) { + onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler, + mRecyclerView.mState, host, info); + } + } + + /** + * Called by the AccessibilityDelegate when the accessibility information for a specific + * item should be populated. + * <p> + * Default implementation adds basic positioning information about the item. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param host The child for which accessibility node info should be populated + * @param info The info to fill out about the item + * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int, + * android.view.accessibility.AccessibilityNodeInfo) + */ + public void onInitializeAccessibilityNodeInfoForItem(Recycler recycler, State state, + View host, AccessibilityNodeInfo info) { + int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0; + int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0; + final AccessibilityNodeInfo.CollectionItemInfo itemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndexGuess, 1, + columnIndexGuess, 1, false, false); + info.setCollectionItemInfo(itemInfo); + } + + /** + * A LayoutManager can call this method to force RecyclerView to run simple animations in + * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data + * change). + * <p> + * Note that, calling this method will not guarantee that RecyclerView will run animations + * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will + * not run any animations but will still clear this flag after the layout is complete. + * + */ + public void requestSimpleAnimationsInNextLayout() { + mRequestedSimpleAnimations = true; + } + + /** + * Returns the selection mode for accessibility. Should be + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}, + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_SINGLE} or + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_MULTIPLE}. + * <p> + * Default implementation returns + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return Selection mode for accessibility. Default implementation returns + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}. + */ + public int getSelectionModeForAccessibility(Recycler recycler, State state) { + return AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE; + } + + /** + * Returns the number of rows for accessibility. + * <p> + * Default implementation returns the number of items in the adapter if LayoutManager + * supports vertical scrolling or 1 if LayoutManager does not support vertical + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getRowCountForAccessibility(Recycler recycler, State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1; + } + + /** + * Returns the number of columns for accessibility. + * <p> + * Default implementation returns the number of items in the adapter if LayoutManager + * supports horizontal scrolling or 1 if LayoutManager does not support horizontal + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getColumnCountForAccessibility(Recycler recycler, State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1; + } + + /** + * Returns whether layout is hierarchical or not to be used for accessibility. + * <p> + * Default implementation returns false. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return True if layout is hierarchical. + */ + public boolean isLayoutHierarchical(Recycler recycler, State state) { + return false; + } + + // called by accessibility delegate + boolean performAccessibilityAction(int action, Bundle args) { + return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState, + action, args); + } + + /** + * Called by AccessibilityDelegate when an action is requested from the RecyclerView. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param action The action to perform + * @param args Optional action arguments + * @see View#performAccessibilityAction(int, android.os.Bundle) + */ + public boolean performAccessibilityAction(Recycler recycler, State state, int action, + Bundle args) { + if (mRecyclerView == null) { + return false; + } + int vScroll = 0, hScroll = 0; + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + if (mRecyclerView.canScrollVertically(-1)) { + vScroll = -(getHeight() - getPaddingTop() - getPaddingBottom()); + } + if (mRecyclerView.canScrollHorizontally(-1)) { + hScroll = -(getWidth() - getPaddingLeft() - getPaddingRight()); + } + break; + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + if (mRecyclerView.canScrollVertically(1)) { + vScroll = getHeight() - getPaddingTop() - getPaddingBottom(); + } + if (mRecyclerView.canScrollHorizontally(1)) { + hScroll = getWidth() - getPaddingLeft() - getPaddingRight(); + } + break; + } + if (vScroll == 0 && hScroll == 0) { + return false; + } + mRecyclerView.scrollBy(hScroll, vScroll); + return true; + } + + // called by accessibility delegate + boolean performAccessibilityActionForItem(View view, int action, Bundle args) { + return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState, + view, action, args); + } + + /** + * Called by AccessibilityDelegate when an accessibility action is requested on one of the + * children of LayoutManager. + * <p> + * Default implementation does not do anything. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param view The child view on which the action is performed + * @param action The action to perform + * @param args Optional action arguments + * @return true if action is handled + * @see View#performAccessibilityAction(int, android.os.Bundle) + */ + public boolean performAccessibilityActionForItem(Recycler recycler, State state, View view, + int action, Bundle args) { + return false; + } + + /** + * Parse the xml attributes to get the most common properties used by layout managers. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + * + * @return an object containing the properties as specified in the attrs. + */ + public static Properties getProperties(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + Properties properties = new Properties(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyleAttr, defStyleRes); + properties.orientation = a.getInt(R.styleable.RecyclerView_orientation, VERTICAL); + properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); + properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); + properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); + a.recycle(); + return properties; + } + + void setExactMeasureSpecsFrom(RecyclerView recyclerView) { + setMeasureSpecs( + MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY) + ); + } + + /** + * Internal API to allow LayoutManagers to be measured twice. + * <p> + * This is not public because LayoutManagers should be able to handle their layouts in one + * pass but it is very convenient to make existing LayoutManagers support wrapping content + * when both orientations are undefined. + * <p> + * This API will be removed after default LayoutManagers properly implement wrap content in + * non-scroll orientation. + */ + boolean shouldMeasureTwice() { + return false; + } + + boolean hasFlexibleChildInBothOrientations() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp.width < 0 && lp.height < 0) { + return true; + } + } + return false; + } + + /** + * Some general properties that a LayoutManager may want to use. + */ + public static class Properties { + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation */ + public int orientation; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount */ + public int spanCount; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout */ + public boolean reverseLayout; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd */ + public boolean stackFromEnd; + } + } + + /** + * An ItemDecoration allows the application to add a special drawing and layout offset + * to specific item views from the adapter's data set. This can be useful for drawing dividers + * between items, highlights, visual grouping boundaries and more. + * + * <p>All ItemDecorations are drawn in the order they were added, before the item + * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()} + * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView, + * RecyclerView.State)}.</p> + */ + public abstract static class ItemDecoration { + /** + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn before the item views are drawn, + * and will thus appear underneath the views. + * + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView + */ + public void onDraw(Canvas c, RecyclerView parent, State state) { + onDraw(c, parent); + } + + /** + * @deprecated + * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDraw(Canvas c, RecyclerView parent) { + } + + /** + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn after the item views are drawn + * and will thus appear over the views. + * + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView. + */ + public void onDrawOver(Canvas c, RecyclerView parent, State state) { + onDrawOver(c, parent); + } + + /** + * @deprecated + * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDrawOver(Canvas c, RecyclerView parent) { + } + + + /** + * @deprecated + * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)} + */ + @Deprecated + public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { + outRect.set(0, 0, 0, 0); + } + + /** + * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies + * the number of pixels that the item view should be inset by, similar to padding or margin. + * The default implementation sets the bounds of outRect to 0 and returns. + * + * <p> + * If this ItemDecoration does not affect the positioning of item views, it should set + * all four fields of <code>outRect</code> (left, top, right, bottom) to zero + * before returning. + * + * <p> + * If you need to access Adapter for additional data, you can call + * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the + * View. + * + * @param outRect Rect to receive the output. + * @param view The child view to decorate + * @param parent RecyclerView this ItemDecoration is decorating + * @param state The current state of RecyclerView. + */ + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) { + getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), + parent); + } + } + + /** + * An OnItemTouchListener allows the application to intercept touch events in progress at the + * view hierarchy level of the RecyclerView before those touch events are considered for + * RecyclerView's own scrolling behavior. + * + * <p>This can be useful for applications that wish to implement various forms of gestural + * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept + * a touch interaction already in progress even if the RecyclerView is already handling that + * gesture stream itself for the purposes of scrolling.</p> + * + * @see SimpleOnItemTouchListener + */ + public interface OnItemTouchListener { + /** + * Silently observe and/or take over touch events sent to the RecyclerView + * before they are handled by either the RecyclerView itself or its child views. + * + * <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run + * in the order in which each listener was added, before any other touch processing + * by the RecyclerView itself or child views occurs.</p> + * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false + * to continue with the current behavior and continue observing future events in + * the gesture. + */ + boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e); + + /** + * Process a touch event as part of a gesture that was claimed by returning true from + * a previous call to {@link #onInterceptTouchEvent}. + * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + */ + void onTouchEvent(RecyclerView rv, MotionEvent e); + + /** + * Called when a child of RecyclerView does not want RecyclerView and its ancestors to + * intercept touch events with + * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. + * + * @param disallowIntercept True if the child does not want the parent to + * intercept touch events. + * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) + */ + void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); + } + + /** + * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies + * and default return values. + * <p> + * You may prefer to extend this class if you don't need to override all methods. Another + * benefit of using this class is future compatibility. As the interface may change, we'll + * always provide a default implementation on this class so that your code won't break when + * you update to a new version of the support library. + */ + public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener { + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } + } + + + /** + * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event + * has occurred on that RecyclerView. + * <p> + * @see RecyclerView#addOnScrollListener(OnScrollListener) + * @see RecyclerView#clearOnChildAttachStateChangeListeners() + * + */ + public abstract static class OnScrollListener { + /** + * Callback method to be invoked when RecyclerView's scroll state changes. + * + * @param recyclerView The RecyclerView whose scroll state has changed. + * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. + */ + public void onScrollStateChanged(RecyclerView recyclerView, int newState){} + + /** + * Callback method to be invoked when the RecyclerView has been scrolled. This will be + * called after the scroll has completed. + * <p> + * This callback will also be called if visible item range changes after a layout + * calculation. In that case, dx and dy will be 0. + * + * @param recyclerView The RecyclerView which scrolled. + * @param dx The amount of horizontal scroll. + * @param dy The amount of vertical scroll. + */ + public void onScrolled(RecyclerView recyclerView, int dx, int dy){} + } + + /** + * A RecyclerListener can be set on a RecyclerView to receive messages whenever + * a view is recycled. + * + * @see RecyclerView#setRecyclerListener(RecyclerListener) + */ + public interface RecyclerListener { + + /** + * This method is called whenever the view in the ViewHolder is recycled. + * + * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder containing the view that was recycled + */ + void onViewRecycled(ViewHolder holder); + } + + /** + * A Listener interface that can be attached to a RecylcerView to get notified + * whenever a ViewHolder is attached to or detached from RecyclerView. + */ + public interface OnChildAttachStateChangeListener { + + /** + * Called when a view is attached to the RecyclerView. + * + * @param view The View which is attached to the RecyclerView + */ + void onChildViewAttachedToWindow(View view); + + /** + * Called when a view is detached from RecyclerView. + * + * @param view The View which is being detached from the RecyclerView + */ + void onChildViewDetachedFromWindow(View view); + } + + /** + * A ViewHolder describes an item view and metadata about its place within the RecyclerView. + * + * <p>{@link Adapter} implementations should subclass ViewHolder and add fields for caching + * potentially expensive {@link View#findViewById(int)} results.</p> + * + * <p>While {@link LayoutParams} belong to the {@link LayoutManager}, + * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use + * their own custom ViewHolder implementations to store data that makes binding view contents + * easier. Implementations should assume that individual item views will hold strong references + * to <code>ViewHolder</code> objects and that <code>RecyclerView</code> instances may hold + * strong references to extra off-screen item views for caching purposes</p> + */ + public abstract static class ViewHolder { + public final View itemView; + WeakReference<RecyclerView> mNestedRecyclerView; + int mPosition = NO_POSITION; + int mOldPosition = NO_POSITION; + long mItemId = NO_ID; + int mItemViewType = INVALID_TYPE; + int mPreLayoutPosition = NO_POSITION; + + // The item that this holder is shadowing during an item change event/animation + ViewHolder mShadowedHolder = null; + // The item that is shadowing this holder during an item change event/animation + ViewHolder mShadowingHolder = null; + + /** + * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType + * are all valid. + */ + static final int FLAG_BOUND = 1 << 0; + + /** + * The data this ViewHolder's view reflects is stale and needs to be rebound + * by the adapter. mPosition and mItemId are consistent. + */ + static final int FLAG_UPDATE = 1 << 1; + + /** + * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId + * are not to be trusted and may no longer match the item view type. + * This ViewHolder must be fully rebound to different data. + */ + static final int FLAG_INVALID = 1 << 2; + + /** + * This ViewHolder points at data that represents an item previously removed from the + * data set. Its view may still be used for things like outgoing animations. + */ + static final int FLAG_REMOVED = 1 << 3; + + /** + * This ViewHolder should not be recycled. This flag is set via setIsRecyclable() + * and is intended to keep views around during animations. + */ + static final int FLAG_NOT_RECYCLABLE = 1 << 4; + + /** + * This ViewHolder is returned from scrap which means we are expecting an addView call + * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until + * the end of the layout pass and then recycled by RecyclerView if it is not added back to + * the RecyclerView. + */ + static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5; + + /** + * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove + * it unless LayoutManager is replaced. + * It is still fully visible to the LayoutManager. + */ + static final int FLAG_IGNORE = 1 << 7; + + /** + * When the View is detached form the parent, we set this flag so that we can take correct + * action when we need to remove it or add it back. + */ + static final int FLAG_TMP_DETACHED = 1 << 8; + + /** + * Set when we can no longer determine the adapter position of this ViewHolder until it is + * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is + * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon + * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is + * re-calculated. + */ + static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9; + + /** + * Set when a addChangePayload(null) is called + */ + static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10; + + /** + * Used by ItemAnimator when a ViewHolder's position changes + */ + static final int FLAG_MOVED = 1 << 11; + + /** + * Used by ItemAnimator when a ViewHolder appears in pre-layout + */ + static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12; + + static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1; + + /** + * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from + * hidden list (as if it was scrap) without being recycled in between. + * + * When a ViewHolder is hidden, there are 2 paths it can be re-used: + * a) Animation ends, view is recycled and used from the recycle pool. + * b) LayoutManager asks for the View for that position while the ViewHolder is hidden. + * + * This flag is used to represent "case b" where the ViewHolder is reused without being + * recycled (thus "bounced" from the hidden list). This state requires special handling + * because the ViewHolder must be added to pre layout maps for animations as if it was + * already there. + */ + static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13; + + private int mFlags; + + private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST; + + List<Object> mPayloads = null; + List<Object> mUnmodifiedPayloads = null; + + private int mIsRecyclableCount = 0; + + // If non-null, view is currently considered scrap and may be reused for other data by the + // scrap container. + private Recycler mScrapContainer = null; + // Keeps whether this ViewHolder lives in Change scrap or Attached scrap + private boolean mInChangeScrap = false; + + // Saves isImportantForAccessibility value for the view item while it's in hidden state and + // marked as unimportant for accessibility. + private int mWasImportantForAccessibilityBeforeHidden = + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + // set if we defer the accessibility state change of the view holder + @VisibleForTesting + int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + + /** + * Is set when VH is bound from the adapter and cleaned right before it is sent to + * {@link RecycledViewPool}. + */ + RecyclerView mOwnerRecyclerView; + + public ViewHolder(View itemView) { + if (itemView == null) { + throw new IllegalArgumentException("itemView may not be null"); + } + this.itemView = itemView; + } + + void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) { + addFlags(ViewHolder.FLAG_REMOVED); + offsetPosition(offset, applyToPreLayout); + mPosition = mNewPosition; + } + + void offsetPosition(int offset, boolean applyToPreLayout) { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; + } + if (mPreLayoutPosition == NO_POSITION) { + mPreLayoutPosition = mPosition; + } + if (applyToPreLayout) { + mPreLayoutPosition += offset; + } + mPosition += offset; + if (itemView.getLayoutParams() != null) { + ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true; + } + } + + void clearOldPosition() { + mOldPosition = NO_POSITION; + mPreLayoutPosition = NO_POSITION; + } + + void saveOldPosition() { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; + } + } + + boolean shouldIgnore() { + return (mFlags & FLAG_IGNORE) != 0; + } + + /** + * @deprecated This method is deprecated because its meaning is ambiguous due to the async + * handling of adapter updates. Please use {@link #getLayoutPosition()} or + * {@link #getAdapterPosition()} depending on your use case. + * + * @see #getLayoutPosition() + * @see #getAdapterPosition() + */ + @Deprecated + public final int getPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } + + /** + * Returns the position of the ViewHolder in terms of the latest layout pass. + * <p> + * This position is mostly used by RecyclerView components to be consistent while + * RecyclerView lazily processes adapter updates. + * <p> + * For performance and animation reasons, RecyclerView batches all adapter updates until the + * next layout pass. This may cause mismatches between the Adapter position of the item and + * the position it had in the latest layout calculations. + * <p> + * LayoutManagers should always call this method while doing calculations based on item + * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State}, + * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position + * of the item. + * <p> + * If LayoutManager needs to call an external method that requires the adapter position of + * the item, it can use {@link #getAdapterPosition()} or + * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. + * + * @return Returns the adapter position of the ViewHolder in the latest layout pass. + * @see #getAdapterPosition() + */ + public final int getLayoutPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder. + * <p> + * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + * <p> + * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + * <p> + * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + * + * @return The adapter position of the item if it still exists in the adapter. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + */ + public final int getAdapterPosition() { + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + return mOwnerRecyclerView.getAdapterPositionFor(this); + } + + /** + * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders + * to perform animations. + * <p> + * If a ViewHolder was laid out in the previous onLayout call, old position will keep its + * adapter index in the previous layout. + * + * @return The previous adapter index of the Item represented by this ViewHolder or + * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is + * complete). + */ + public final int getOldPosition() { + return mOldPosition; + } + + /** + * Returns The itemId represented by this ViewHolder. + * + * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID} + * otherwise + */ + public final long getItemId() { + return mItemId; + } + + /** + * @return The view type of this ViewHolder. + */ + public final int getItemViewType() { + return mItemViewType; + } + + boolean isScrap() { + return mScrapContainer != null; + } + + void unScrap() { + mScrapContainer.unscrapView(this); + } + + boolean wasReturnedFromScrap() { + return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0; + } + + void clearReturnedFromScrapFlag() { + mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP; + } + + void clearTmpDetachFlag() { + mFlags = mFlags & ~FLAG_TMP_DETACHED; + } + + void stopIgnoring() { + mFlags = mFlags & ~FLAG_IGNORE; + } + + void setScrapContainer(Recycler recycler, boolean isChangeScrap) { + mScrapContainer = recycler; + mInChangeScrap = isChangeScrap; + } + + boolean isInvalid() { + return (mFlags & FLAG_INVALID) != 0; + } + + boolean needsUpdate() { + return (mFlags & FLAG_UPDATE) != 0; + } + + boolean isBound() { + return (mFlags & FLAG_BOUND) != 0; + } + + boolean isRemoved() { + return (mFlags & FLAG_REMOVED) != 0; + } + + boolean hasAnyOfTheFlags(int flags) { + return (mFlags & flags) != 0; + } + + boolean isTmpDetached() { + return (mFlags & FLAG_TMP_DETACHED) != 0; + } + + boolean isAdapterPositionUnknown() { + return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid(); + } + + void setFlags(int flags, int mask) { + mFlags = (mFlags & ~mask) | (flags & mask); + } + + void addFlags(int flags) { + mFlags |= flags; + } + + void addChangePayload(Object payload) { + if (payload == null) { + addFlags(FLAG_ADAPTER_FULLUPDATE); + } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + createPayloadsIfNeeded(); + mPayloads.add(payload); + } + } + + private void createPayloadsIfNeeded() { + if (mPayloads == null) { + mPayloads = new ArrayList<Object>(); + mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads); + } + } + + void clearPayload() { + if (mPayloads != null) { + mPayloads.clear(); + } + mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE; + } + + List<Object> getUnmodifiedPayloads() { + if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + if (mPayloads == null || mPayloads.size() == 0) { + // Initial state, no update being called. + return FULLUPDATE_PAYLOADS; + } + // there are none-null payloads + return mUnmodifiedPayloads; + } else { + // a full update has been called. + return FULLUPDATE_PAYLOADS; + } + } + + void resetInternal() { + mFlags = 0; + mPosition = NO_POSITION; + mOldPosition = NO_POSITION; + mItemId = NO_ID; + mPreLayoutPosition = NO_POSITION; + mIsRecyclableCount = 0; + mShadowedHolder = null; + mShadowingHolder = null; + clearPayload(); + mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + clearNestedRecyclerViewIfNotNested(this); + } + + /** + * Called when the child view enters the hidden state + */ + private void onEnteredHiddenState(RecyclerView parent) { + // While the view item is in hidden state, make it invisible for the accessibility. + mWasImportantForAccessibilityBeforeHidden = + itemView.getImportantForAccessibility(); + parent.setChildImportantForAccessibilityInternal(this, + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + + /** + * Called when the child view leaves the hidden state + */ + private void onLeftHiddenState(RecyclerView parent) { + parent.setChildImportantForAccessibilityInternal(this, + mWasImportantForAccessibilityBeforeHidden); + mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ViewHolder{" + + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId + + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); + if (isScrap()) { + sb.append(" scrap ") + .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); + } + if (isInvalid()) sb.append(" invalid"); + if (!isBound()) sb.append(" unbound"); + if (needsUpdate()) sb.append(" update"); + if (isRemoved()) sb.append(" removed"); + if (shouldIgnore()) sb.append(" ignored"); + if (isTmpDetached()) sb.append(" tmpDetached"); + if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")"); + if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); + + if (itemView.getParent() == null) sb.append(" no parent"); + sb.append("}"); + return sb.toString(); + } + + /** + * Informs the recycler whether this item can be recycled. Views which are not + * recyclable will not be reused for other items until setIsRecyclable() is + * later set to true. Calls to setIsRecyclable() should always be paired (one + * call to setIsRecyclabe(false) should always be matched with a later call to + * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally + * reference-counted. + * + * @param recyclable Whether this item is available to be recycled. Default value + * is true. + * + * @see #isRecyclable() + */ + public final void setIsRecyclable(boolean recyclable) { + mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1; + if (mIsRecyclableCount < 0) { + mIsRecyclableCount = 0; + if (DEBUG) { + throw new RuntimeException("isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } + Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } else if (!recyclable && mIsRecyclableCount == 1) { + mFlags |= FLAG_NOT_RECYCLABLE; + } else if (recyclable && mIsRecyclableCount == 0) { + mFlags &= ~FLAG_NOT_RECYCLABLE; + } + if (DEBUG) { + Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this); + } + } + + /** + * @return true if this item is available to be recycled, false otherwise. + * + * @see #setIsRecyclable(boolean) + */ + public final boolean isRecyclable() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 + && !itemView.hasTransientState(); + } + + /** + * Returns whether we have animations referring to this view holder or not. + * This is similar to isRecyclable flag but does not check transient state. + */ + private boolean shouldBeKeptAsChild() { + return (mFlags & FLAG_NOT_RECYCLABLE) != 0; + } + + /** + * @return True if ViewHolder is not referenced by RecyclerView animations but has + * transient state which will prevent it from being recycled. + */ + private boolean doesTransientStatePreventRecycling() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && itemView.hasTransientState(); + } + + boolean isUpdated() { + return (mFlags & FLAG_UPDATE) != 0; + } + } + + /** + * This method is here so that we can control the important for a11y changes and test it. + */ + @VisibleForTesting + boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, + int importantForAccessibility) { + if (isComputingLayout()) { + viewHolder.mPendingAccessibilityState = importantForAccessibility; + mPendingAccessibilityImportanceChange.add(viewHolder); + return false; + } + viewHolder.itemView.setImportantForAccessibility(importantForAccessibility); + return true; + } + + void dispatchPendingImportantForAccessibilityChanges() { + for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) { + ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i); + if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) { + continue; + } + int state = viewHolder.mPendingAccessibilityState; + if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) { + //noinspection WrongConstant + viewHolder.itemView.setImportantForAccessibility(state); + viewHolder.mPendingAccessibilityState = + ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET; + } + } + mPendingAccessibilityImportanceChange.clear(); + } + + int getAdapterPositionFor(ViewHolder viewHolder) { + if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) + || !viewHolder.isBound()) { + return RecyclerView.NO_POSITION; + } + return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); + } + + /** + * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of + * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged + * to create their own subclass of this <code>LayoutParams</code> class + * to store any additional required per-child view metadata about the layout. + */ + public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { + ViewHolder mViewHolder; + final Rect mDecorInsets = new Rect(); + boolean mInsetsDirty = true; + // Flag is set to true if the view is bound while it is detached from RV. + // In this case, we need to manually call invalidate after view is added to guarantee that + // invalidation is populated through the View hierarchy + boolean mPendingInvalidate = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super((ViewGroup.LayoutParams) source); + } + + /** + * Returns true if the view this LayoutParams is attached to needs to have its content + * updated from the corresponding adapter. + * + * @return true if the view should have its content updated + */ + public boolean viewNeedsUpdate() { + return mViewHolder.needsUpdate(); + } + + /** + * Returns true if the view this LayoutParams is attached to is now representing + * potentially invalid data. A LayoutManager should scrap/recycle it. + * + * @return true if the view is invalid + */ + public boolean isViewInvalid() { + return mViewHolder.isInvalid(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been removed from the data set. A LayoutManager may choose to + * treat it differently in order to animate its outgoing or disappearing state. + * + * @return true if the item the view corresponds to was removed from the data set + */ + public boolean isItemRemoved() { + return mViewHolder.isRemoved(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been changed in the data set. A LayoutManager may choose to + * treat it differently in order to animate its changing state. + * + * @return true if the item the view corresponds to was changed in the data set + */ + public boolean isItemChanged() { + return mViewHolder.isUpdated(); + } + + /** + * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} + */ + @Deprecated + public int getViewPosition() { + return mViewHolder.getPosition(); + } + + /** + * Returns the adapter position that the view this LayoutParams is attached to corresponds + * to as of latest layout calculation. + * + * @return the adapter position this view as of latest layout pass + */ + public int getViewLayoutPosition() { + return mViewHolder.getLayoutPosition(); + } + + /** + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to. + * + * @return the up-to-date adapter position this view. It may return + * {@link RecyclerView#NO_POSITION} if item represented by this View has been removed or + * its up-to-date position cannot be calculated. + */ + public int getViewAdapterPosition() { + return mViewHolder.getAdapterPosition(); + } + } + + /** + * Observer base class for watching changes to an {@link Adapter}. + * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}. + */ + public abstract static class AdapterDataObserver { + public void onChanged() { + // Do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + // fallback to onItemRangeChanged(positionStart, itemCount) if app + // does not override this method. + onItemRangeChanged(positionStart, itemCount); + } + + public void onItemRangeInserted(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeRemoved(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + // do nothing + } + } + + /** + * <p>Base class for smooth scrolling. Handles basic tracking of the target view position and + * provides methods to trigger a programmatic scroll.</p> + * + * @see LinearSmoothScroller + */ + public abstract static class SmoothScroller { + + private int mTargetPosition = RecyclerView.NO_POSITION; + + private RecyclerView mRecyclerView; + + private LayoutManager mLayoutManager; + + private boolean mPendingInitialRun; + + private boolean mRunning; + + private View mTargetView; + + private final Action mRecyclingAction; + + public SmoothScroller() { + mRecyclingAction = new Action(0, 0); + } + + /** + * Starts a smooth scroll for the given target position. + * <p>In each animation step, {@link RecyclerView} will check + * for the target view and call either + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until + * SmoothScroller is stopped.</p> + * + * <p>Note that if RecyclerView finds the target view, it will automatically stop the + * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will + * stop calling SmoothScroller in each animation step.</p> + */ + void start(RecyclerView recyclerView, LayoutManager layoutManager) { + mRecyclerView = recyclerView; + mLayoutManager = layoutManager; + if (mTargetPosition == RecyclerView.NO_POSITION) { + throw new IllegalArgumentException("Invalid target position"); + } + mRecyclerView.mState.mTargetPosition = mTargetPosition; + mRunning = true; + mPendingInitialRun = true; + mTargetView = findViewByPosition(getTargetPosition()); + onStart(); + mRecyclerView.mViewFlinger.postOnAnimation(); + } + + public void setTargetPosition(int targetPosition) { + mTargetPosition = targetPosition; + } + + /** + * @return The LayoutManager to which this SmoothScroller is attached. Will return + * <code>null</code> after the SmoothScroller is stopped. + */ + @Nullable + public LayoutManager getLayoutManager() { + return mLayoutManager; + } + + /** + * Stops running the SmoothScroller in each animation callback. Note that this does not + * cancel any existing {@link Action} updated by + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}. + */ + protected final void stop() { + if (!mRunning) { + return; + } + onStop(); + mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION; + mTargetView = null; + mTargetPosition = RecyclerView.NO_POSITION; + mPendingInitialRun = false; + mRunning = false; + // trigger a cleanup + mLayoutManager.onSmoothScrollerStopped(this); + // clear references to avoid any potential leak by a custom smooth scroller + mLayoutManager = null; + mRecyclerView = null; + } + + /** + * Returns true if SmoothScroller has been started but has not received the first + * animation + * callback yet. + * + * @return True if this SmoothScroller is waiting to start + */ + public boolean isPendingInitialRun() { + return mPendingInitialRun; + } + + + /** + * @return True if SmoothScroller is currently active + */ + public boolean isRunning() { + return mRunning; + } + + /** + * Returns the adapter position of the target item + * + * @return Adapter position of the target item or + * {@link RecyclerView#NO_POSITION} if no target view is set. + */ + public int getTargetPosition() { + return mTargetPosition; + } + + private void onAnimation(int dx, int dy) { + final RecyclerView recyclerView = mRecyclerView; + if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) { + stop(); + } + mPendingInitialRun = false; + if (mTargetView != null) { + // verify target position + if (getChildPosition(mTargetView) == mTargetPosition) { + onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(recyclerView); + stop(); + } else { + Log.e(TAG, "Passed over target position while smooth scrolling."); + mTargetView = null; + } + } + if (mRunning) { + onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction); + boolean hadJumpTarget = mRecyclingAction.hasJumpTarget(); + mRecyclingAction.runIfNecessary(recyclerView); + if (hadJumpTarget) { + // It is not stopped so needs to be restarted + if (mRunning) { + mPendingInitialRun = true; + recyclerView.mViewFlinger.postOnAnimation(); + } else { + stop(); // done + } + } + } + } + + /** + * @see RecyclerView#getChildLayoutPosition(android.view.View) + */ + public int getChildPosition(View view) { + return mRecyclerView.getChildLayoutPosition(view); + } + + /** + * @see RecyclerView.LayoutManager#getChildCount() + */ + public int getChildCount() { + return mRecyclerView.mLayout.getChildCount(); + } + + /** + * @see RecyclerView.LayoutManager#findViewByPosition(int) + */ + public View findViewByPosition(int position) { + return mRecyclerView.mLayout.findViewByPosition(position); + } + + /** + * @see RecyclerView#scrollToPosition(int) + * @deprecated Use {@link Action#jumpTo(int)}. + */ + @Deprecated + public void instantScrollToPosition(int position) { + mRecyclerView.scrollToPosition(position); + } + + protected void onChildAttachedToWindow(View child) { + if (getChildPosition(child) == getTargetPosition()) { + mTargetView = child; + if (DEBUG) { + Log.d(TAG, "smooth scroll target view has been attached"); + } + } + } + + /** + * Normalizes the vector. + * @param scrollVector The vector that points to the target scroll position + */ + protected void normalize(PointF scrollVector) { + final double magnitude = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y + * scrollVector.y); + scrollVector.x /= magnitude; + scrollVector.y /= magnitude; + } + + /** + * Called when smooth scroll is started. This might be a good time to do setup. + */ + protected abstract void onStart(); + + /** + * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. + * @see #stop() + */ + protected abstract void onStop(); + + /** + * <p>RecyclerView will call this method each time it scrolls until it can find the target + * position in the layout.</p> + * <p>SmoothScroller should check dx, dy and if scroll should be changed, update the + * provided {@link Action} to define the next scroll.</p> + * + * @param dx Last scroll amount horizontally + * @param dy Last scroll amount vertically + * @param state Transient state of RecyclerView + * @param action If you want to trigger a new smooth scroll and cancel the previous one, + * update this object. + */ + protected abstract void onSeekTargetStep(int dx, int dy, State state, Action action); + + /** + * Called when the target position is laid out. This is the last callback SmoothScroller + * will receive and it should update the provided {@link Action} to define the scroll + * details towards the target view. + * @param targetView The view element which render the target position. + * @param state Transient state of RecyclerView + * @param action Action instance that you should update to define final scroll action + * towards the targetView + */ + protected abstract void onTargetFound(View targetView, State state, Action action); + + /** + * Holds information about a smooth scroll request by a {@link SmoothScroller}. + */ + public static class Action { + + public static final int UNDEFINED_DURATION = Integer.MIN_VALUE; + + private int mDx; + + private int mDy; + + private int mDuration; + + private int mJumpToPosition = NO_POSITION; + + private Interpolator mInterpolator; + + private boolean mChanged = false; + + // we track this variable to inform custom implementer if they are updating the action + // in every animation callback + private int mConsecutiveUpdates = 0; + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + */ + public Action(int dx, int dy) { + this(dx, dy, UNDEFINED_DURATION, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + */ + public Action(int dx, int dy, int duration) { + this(dx, dy, duration, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step + */ + public Action(int dx, int dy, int duration, Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + } + + /** + * Instead of specifying pixels to scroll, use the target position to jump using + * {@link RecyclerView#scrollToPosition(int)}. + * <p> + * You may prefer using this method if scroll target is really far away and you prefer + * to jump to a location and smooth scroll afterwards. + * <p> + * Note that calling this method takes priority over other update methods such as + * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)}, + * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call + * {@link #jumpTo(int)}, the other changes will not be considered for this animation + * frame. + * + * @param targetPosition The target item position to scroll to using instant scrolling. + */ + public void jumpTo(int targetPosition) { + mJumpToPosition = targetPosition; + } + + boolean hasJumpTarget() { + return mJumpToPosition >= 0; + } + + void runIfNecessary(RecyclerView recyclerView) { + if (mJumpToPosition >= 0) { + final int position = mJumpToPosition; + mJumpToPosition = NO_POSITION; + recyclerView.jumpToPositionForSmoothScroller(position); + mChanged = false; + return; + } + if (mChanged) { + validate(); + if (mInterpolator == null) { + if (mDuration == UNDEFINED_DURATION) { + recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy); + } else { + recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration); + } + } else { + recyclerView.mViewFlinger.smoothScrollBy( + mDx, mDy, mDuration, mInterpolator); + } + mConsecutiveUpdates++; + if (mConsecutiveUpdates > 10) { + // A new action is being set in every animation step. This looks like a bad + // implementation. Inform developer. + Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure" + + " you are not changing it unless necessary"); + } + mChanged = false; + } else { + mConsecutiveUpdates = 0; + } + } + + private void validate() { + if (mInterpolator != null && mDuration < 1) { + throw new IllegalStateException("If you provide an interpolator, you must" + + " set a positive duration"); + } else if (mDuration < 1) { + throw new IllegalStateException("Scroll duration must be a positive number"); + } + } + + public int getDx() { + return mDx; + } + + public void setDx(int dx) { + mChanged = true; + mDx = dx; + } + + public int getDy() { + return mDy; + } + + public void setDy(int dy) { + mChanged = true; + mDy = dy; + } + + public int getDuration() { + return mDuration; + } + + public void setDuration(int duration) { + mChanged = true; + mDuration = duration; + } + + public Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * Sets the interpolator to calculate scroll steps + * @param interpolator The interpolator to use. If you specify an interpolator, you must + * also set the duration. + * @see #setDuration(int) + */ + public void setInterpolator(Interpolator interpolator) { + mChanged = true; + mInterpolator = interpolator; + } + + /** + * Updates the action with given parameters. + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step + */ + public void update(int dx, int dy, int duration, Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + mChanged = true; + } + } + + /** + * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager} + * to provide a hint to a {@link SmoothScroller} about the location of the target position. + */ + public interface ScrollVectorProvider { + /** + * Should calculate the vector that points to the direction where the target position + * can be found. + * <p> + * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards + * the target position. + * <p> + * The magnitude of the vector is not important. It is always normalized before being + * used by the {@link LinearSmoothScroller}. + * <p> + * LayoutManager should not check whether the position exists in the adapter or not. + * + * @param targetPosition the target position to which the returned vector should point + * + * @return the scroll vector for a given position. + */ + PointF computeScrollVectorForPosition(int targetPosition); + } + } + + static class AdapterDataObservable extends Observable<AdapterDataObserver> { + public boolean hasObservers() { + return !mObservers.isEmpty(); + } + + public void notifyChanged() { + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); + } + } + + public void notifyItemRangeChanged(int positionStart, int itemCount) { + notifyItemRangeChanged(positionStart, itemCount, null); + } + + public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { + // since onItemRangeChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); + } + } + + public void notifyItemRangeInserted(int positionStart, int itemCount) { + // since onItemRangeInserted() is implemented by the app, it could do anything, + // including removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeInserted(positionStart, itemCount); + } + } + + public void notifyItemRangeRemoved(int positionStart, int itemCount) { + // since onItemRangeRemoved() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeRemoved(positionStart, itemCount); + } + } + + public void notifyItemMoved(int fromPosition, int toPosition) { + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1); + } + } + } + + /** + * This is public so that the CREATOR can be access on cold launch. + * @hide + */ + public static class SavedState extends AbsSavedState { + + Parcelable mLayoutState; + + /** + * called by CREATOR + */ + SavedState(Parcel in) { + super(in); + mLayoutState = in.readParcelable(LayoutManager.class.getClassLoader()); + } + + /** + * Called by onSaveInstanceState + */ + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(mLayoutState, 0); + } + + void copyFrom(SavedState other) { + mLayoutState = other.mLayoutState; + } + + public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + /** + * <p>Contains useful information about the current RecyclerView state like target scroll + * position or view focus. State object can also keep arbitrary data, identified by resource + * ids.</p> + * <p>Often times, RecyclerView components will need to pass information between each other. + * To provide a well defined data bus between components, RecyclerView passes the same State + * object to component callbacks and these components can use it to exchange data.</p> + * <p>If you implement custom components, you can use State's put/get/remove methods to pass + * data between your components without needing to manage their lifecycles.</p> + */ + public static class State { + static final int STEP_START = 1; + static final int STEP_LAYOUT = 1 << 1; + static final int STEP_ANIMATIONS = 1 << 2; + + void assertLayoutStep(int accepted) { + if ((accepted & mLayoutStep) == 0) { + throw new IllegalStateException("Layout state should be one of " + + Integer.toBinaryString(accepted) + " but it is " + + Integer.toBinaryString(mLayoutStep)); + } + } + + + /** Owned by SmoothScroller */ + private int mTargetPosition = RecyclerView.NO_POSITION; + + private SparseArray<Object> mData; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are carried from one layout pass to the next + //////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Number of items adapter had in the previous layout. + */ + int mPreviousLayoutItemCount = 0; + + /** + * Number of items that were NOT laid out but has been deleted from the adapter after the + * previous layout. + */ + int mDeletedInvisibleItemCountSincePreviousLayout = 0; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below must be updated or cleared before they are used (generally before a pass) + //////////////////////////////////////////////////////////////////////////////////////////// + + @IntDef(flag = true, value = { + STEP_START, STEP_LAYOUT, STEP_ANIMATIONS + }) + @Retention(RetentionPolicy.SOURCE) + @interface LayoutState {} + + @LayoutState + int mLayoutStep = STEP_START; + + /** + * Number of items adapter has. + */ + int mItemCount = 0; + + boolean mStructureChanged = false; + + boolean mInPreLayout = false; + + boolean mTrackOldChangeHolders = false; + + boolean mIsMeasuring = false; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are always reset outside of the pass (or passes) that use them + //////////////////////////////////////////////////////////////////////////////////////////// + + boolean mRunSimpleAnimations = false; + + boolean mRunPredictiveAnimations = false; + + /** + * This data is saved before a layout calculation happens. After the layout is finished, + * if the previously focused view has been replaced with another view for the same item, we + * move the focus to the new item automatically. + */ + int mFocusedItemPosition; + long mFocusedItemId; + // when a sub child has focus, record its id and see if we can directly request focus on + // that one instead + int mFocusedSubChildId; + + //////////////////////////////////////////////////////////////////////////////////////////// + + State reset() { + mTargetPosition = RecyclerView.NO_POSITION; + if (mData != null) { + mData.clear(); + } + mItemCount = 0; + mStructureChanged = false; + mIsMeasuring = false; + return this; + } + + /** + * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially + * prior to any layout passes. + * + * <p>Don't touch any state stored between layout passes, only reset per-layout state, so + * that Recycler#getViewForPosition() can function safely.</p> + */ + void prepareForNestedPrefetch(Adapter adapter) { + mLayoutStep = STEP_START; + mItemCount = adapter.getItemCount(); + mStructureChanged = false; + mInPreLayout = false; + mTrackOldChangeHolders = false; + mIsMeasuring = false; + } + + /** + * Returns true if the RecyclerView is currently measuring the layout. This value is + * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView + * has non-exact measurement specs. + * <p> + * Note that if the LayoutManager supports predictive animations and it is calculating the + * pre-layout step, this value will be {@code false} even if the RecyclerView is in + * {@code onMeasure} call. This is because pre-layout means the previous state of the + * RecyclerView and measurements made for that state cannot change the RecyclerView's size. + * LayoutManager is always guaranteed to receive another call to + * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens. + * + * @return True if the RecyclerView is currently calculating its bounds, false otherwise. + */ + public boolean isMeasuring() { + return mIsMeasuring; + } + + /** + * Returns true if + * @return + */ + public boolean isPreLayout() { + return mInPreLayout; + } + + /** + * Returns whether RecyclerView will run predictive animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating predictive animations to be run at the end + * of the layout pass. + */ + public boolean willRunPredictiveAnimations() { + return mRunPredictiveAnimations; + } + + /** + * Returns whether RecyclerView will run simple animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating simple animations to be run at the end of + * the layout pass. + */ + public boolean willRunSimpleAnimations() { + return mRunSimpleAnimations; + } + + /** + * Removes the mapping from the specified id, if there was any. + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + */ + public void remove(int resourceId) { + if (mData == null) { + return; + } + mData.remove(resourceId); + } + + /** + * Gets the Object mapped from the specified id, or <code>null</code> + * if no such data exists. + * + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* + * to + * preserve cross functionality and avoid conflicts. + */ + public <T> T get(int resourceId) { + if (mData == null) { + return null; + } + return (T) mData.get(resourceId); + } + + /** + * Adds a mapping from the specified id to the specified value, replacing the previous + * mapping from the specified key if there was one. + * + * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + * @param data The data you want to associate with the resourceId. + */ + public void put(int resourceId, Object data) { + if (mData == null) { + mData = new SparseArray<Object>(); + } + mData.put(resourceId, data); + } + + /** + * If scroll is triggered to make a certain item visible, this value will return the + * adapter index of that item. + * @return Adapter index of the target item or + * {@link RecyclerView#NO_POSITION} if there is no target + * position. + */ + public int getTargetScrollPosition() { + return mTargetPosition; + } + + /** + * Returns if current scroll has a target position. + * @return true if scroll is being triggered to make a certain position visible + * @see #getTargetScrollPosition() + */ + public boolean hasTargetScrollPosition() { + return mTargetPosition != RecyclerView.NO_POSITION; + } + + /** + * @return true if the structure of the data set has changed since the last call to + * onLayoutChildren, false otherwise + */ + public boolean didStructureChange() { + return mStructureChanged; + } + + /** + * Returns the total number of items that can be laid out. Note that this number is not + * necessarily equal to the number of items in the adapter, so you should always use this + * number for your position calculations and never access the adapter directly. + * <p> + * RecyclerView listens for Adapter's notify events and calculates the effects of adapter + * data changes on existing Views. These calculations are used to decide which animations + * should be run. + * <p> + * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to + * present the correct state to LayoutManager in pre-layout pass. + * <p> + * For example, a newly added item is not included in pre-layout item count because + * pre-layout reflects the contents of the adapter before the item is added. Behind the + * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that + * LayoutManager does not know about the new item's existence in pre-layout. The item will + * be available in second layout pass and will be included in the item count. Similar + * adjustments are made for moved and removed items as well. + * <p> + * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method. + * + * @return The number of items currently available + * @see LayoutManager#getItemCount() + */ + public int getItemCount() { + return mInPreLayout + ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) + : mItemCount; + } + + @Override + public String toString() { + return "State{" + + "mTargetPosition=" + mTargetPosition + + ", mData=" + mData + + ", mItemCount=" + mItemCount + + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + + ", mDeletedInvisibleItemCountSincePreviousLayout=" + + mDeletedInvisibleItemCountSincePreviousLayout + + ", mStructureChanged=" + mStructureChanged + + ", mInPreLayout=" + mInPreLayout + + ", mRunSimpleAnimations=" + mRunSimpleAnimations + + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations + + '}'; + } + } + + /** + * This class defines the behavior of fling if the developer wishes to handle it. + * <p> + * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior. + * + * @see #setOnFlingListener(OnFlingListener) + */ + public abstract static class OnFlingListener { + + /** + * Override this to handle a fling given the velocities in both x and y directions. + * Note that this method will only be called if the associated {@link LayoutManager} + * supports scrolling and the fling is not handled by nested scrolls first. + * + * @param velocityX the fling velocity on the X axis + * @param velocityY the fling velocity on the Y axis + * + * @return true if the fling washandled, false otherwise. + */ + public abstract boolean onFling(int velocityX, int velocityY); + } + + /** + * Internal listener that manages items after animations finish. This is how items are + * retained (not recycled) during animations, but allowed to be recycled afterwards. + * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished() + * method on the animator's listener when it is done animating any item. + */ + private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { + + ItemAnimatorRestoreListener() { + } + + @Override + public void onAnimationFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh + item.mShadowedHolder = null; + } + // always null this because an OldViewHolder can never become NewViewHolder w/o being + // recycled. + item.mShadowingHolder = null; + if (!item.shouldBeKeptAsChild()) { + if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { + removeDetachedView(item.itemView, false); + } + } + } + } + + /** + * This class defines the animations that take place on items as changes are made + * to the adapter. + * + * Subclasses of ItemAnimator can be used to implement custom animations for actions on + * ViewHolder items. The RecyclerView will manage retaining these items while they + * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)} + * when a ViewHolder's animation is finished. In other words, there must be a matching + * {@link #dispatchAnimationFinished(ViewHolder)} call for each + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()}, + * and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()} call. + * + * <p>By default, RecyclerView uses {@link DefaultItemAnimator}.</p> + * + * @see #setItemAnimator(ItemAnimator) + */ + @SuppressWarnings("UnusedParameters") + public abstract static class ItemAnimator { + + /** + * The Item represented by this ViewHolder is updated. + * <p> + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE; + + /** + * The Item represented by this ViewHolder is removed from the adapter. + * <p> + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED; + + /** + * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content + * represented by this ViewHolder is invalid. + * <p> + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID; + + /** + * The position of the Item represented by this ViewHolder has been changed. This flag is + * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to + * any adapter change that may have a side effect on this item. (e.g. The item before this + * one has been removed from the Adapter). + * <p> + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED; + + /** + * This ViewHolder was not laid out but has been added to the layout in pre-layout state + * by the {@link LayoutManager}. This means that the item was already in the Adapter but + * invisible and it may become visible in the post layout phase. LayoutManagers may prefer + * to add new items in pre-layout to specify their virtual location when they are invisible + * (e.g. to specify the item should <i>animate in</i> from below the visible area). + * <p> + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_APPEARED_IN_PRE_LAYOUT = + ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT; + + /** + * The set of flags that might be passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + */ + @IntDef(flag = true, value = { + FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED, + FLAG_APPEARED_IN_PRE_LAYOUT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AdapterChanges {} + private ItemAnimatorListener mListener = null; + private ArrayList<ItemAnimatorFinishedListener> mFinishedListeners = + new ArrayList<ItemAnimatorFinishedListener>(); + + private long mAddDuration = 120; + private long mRemoveDuration = 120; + private long mMoveDuration = 250; + private long mChangeDuration = 250; + + /** + * Gets the current duration for which all move animations will run. + * + * @return The current move duration + */ + public long getMoveDuration() { + return mMoveDuration; + } + + /** + * Sets the duration for which all move animations will run. + * + * @param moveDuration The move duration + */ + public void setMoveDuration(long moveDuration) { + mMoveDuration = moveDuration; + } + + /** + * Gets the current duration for which all add animations will run. + * + * @return The current add duration + */ + public long getAddDuration() { + return mAddDuration; + } + + /** + * Sets the duration for which all add animations will run. + * + * @param addDuration The add duration + */ + public void setAddDuration(long addDuration) { + mAddDuration = addDuration; + } + + /** + * Gets the current duration for which all remove animations will run. + * + * @return The current remove duration + */ + public long getRemoveDuration() { + return mRemoveDuration; + } + + /** + * Sets the duration for which all remove animations will run. + * + * @param removeDuration The remove duration + */ + public void setRemoveDuration(long removeDuration) { + mRemoveDuration = removeDuration; + } + + /** + * Gets the current duration for which all change animations will run. + * + * @return The current change duration + */ + public long getChangeDuration() { + return mChangeDuration; + } + + /** + * Sets the duration for which all change animations will run. + * + * @param changeDuration The change duration + */ + public void setChangeDuration(long changeDuration) { + mChangeDuration = changeDuration; + } + + /** + * Internal only: + * Sets the listener that must be called when the animator is finished + * animating the item (or immediately if no animation happens). This is set + * internally and is not intended to be set by external code. + * + * @param listener The listener that must be called. + */ + void setListener(ItemAnimatorListener listener) { + mListener = listener; + } + + /** + * Called by the RecyclerView before the layout begins. Item animator should record + * necessary information about the View before it is potentially rebound, moved or removed. + * <p> + * The data returned from this method will be passed to the related <code>animate**</code> + * methods. + * <p> + * Note that this method may be called after pre-layout phase if LayoutManager adds new + * Views to the layout in pre-layout pass. + * <p> + * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View and the adapter change flags. + * + * @param state The current State of RecyclerView which includes some useful data + * about the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * @param changeFlags Additional information about what changes happened in the Adapter + * about the Item represented by this ViewHolder. For instance, if + * item is deleted from the adapter, {@link #FLAG_REMOVED} will be set. + * @param payloads The payload list that was previously passed to + * {@link Adapter#notifyItemChanged(int, Object)} or + * {@link Adapter#notifyItemRangeChanged(int, int, Object)}. + * + * @return An ItemHolderInfo instance that preserves necessary information about the + * ViewHolder. This object will be passed back to related <code>animate**</code> methods + * after layout is complete. + * + * @see #recordPostLayoutInformation(State, ViewHolder) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags, + @NonNull List<Object> payloads) { + return obtainHolderInfo().setFrom(viewHolder); + } + + /** + * Called by the RecyclerView after the layout is complete. Item animator should record + * necessary information about the View's final state. + * <p> + * The data returned from this method will be passed to the related <code>animate**</code> + * methods. + * <p> + * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View. + * + * @param state The current State of RecyclerView which includes some useful data about + * the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * + * @return An ItemHolderInfo that preserves necessary information about the ViewHolder. + * This object will be passed back to related <code>animate**</code> methods when + * RecyclerView decides how items should be animated. + * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public @NonNull ItemHolderInfo recordPostLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder) { + return obtainHolderInfo().setFrom(viewHolder); + } + + /** + * Called by the RecyclerView when a ViewHolder has disappeared from the layout. + * <p> + * This means that the View was a child of the LayoutManager when layout started but has + * been removed by the LayoutManager. It might have been removed from the adapter or simply + * become invisible due to other factors. You can distinguish these two cases by checking + * the change flags that were passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * <p> + * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator + * returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + * <p> + * If LayoutManager supports predictive animations, it might provide a target disappear + * location for the View by laying it out in that location. When that happens, + * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the + * response of that call will be passed to this method as the <code>postLayoutInfo</code>. + * <p> + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from + * {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be + * null if the LayoutManager did not layout the item. + * + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when a ViewHolder is added to the layout. + * <p> + * In detail, this means that the ViewHolder was <b>not</b> a child when the layout started + * but has been added by the LayoutManager. It might be newly added to the adapter or + * simply become visible due to other factors. + * <p> + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * Might be null if Item was just added to the adapter or + * LayoutManager does not support predictive animations or it could + * not predict that this ViewHolder will become visible. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when a ViewHolder is present in both before and after the + * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call + * for it or a {@link Adapter#notifyDataSetChanged()} call. + * <p> + * This ViewHolder still represents the same data that it was representing when the layout + * started but its position / size may be changed by the LayoutManager. + * <p> + * If the Item's layout position didn't change, RecyclerView still calls this method because + * it does not track this information (or does not necessarily know that an animation is + * not required). Your ItemAnimator should handle this case and if there is nothing to + * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return + * <code>false</code>. + * <p> + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when an adapter item is present both before and after the + * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call + * for it. This method may also be called when + * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that + * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when + * {@link Adapter#notifyDataSetChanged()} is called, this method <b>will not</b> be called, + * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be + * called for the new ViewHolder and the old one will be recycled. + * <p> + * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is + * a good possibility that item contents didn't really change but it is rebound from the + * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the + * screen didn't change and your animator should handle this case as well and avoid creating + * unnecessary animations. + * <p> + * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the + * previous presentation of the item as-is and supply a new ViewHolder for the updated + * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}. + * This is useful if you don't know the contents of the Item and would like + * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique). + * <p> + * When you are writing a custom item animator for your layout, it might be more performant + * and elegant to re-use the same ViewHolder and animate the content changes manually. + * <p> + * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change. + * If the Item's view type has changed or ItemAnimator returned <code>false</code> for + * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the + * <code>oldHolder</code> and <code>newHolder</code> will be different ViewHolder instances + * which represent the same Item. In that case, only the new ViewHolder is visible + * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations. + * <p> + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct + * ViewHolder when their animation is complete + * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to + * animate the view). + * <p> + * If oldHolder and newHolder are the same instance, you should call + * {@link #dispatchAnimationFinished(ViewHolder)} <b>only once</b>. + * <p> + * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@code animateChange} instead of + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance} if and only if the ItemAnimator returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + * + * @param oldHolder The ViewHolder before the layout is started, might be the same + * instance with newHolder. + * @param newHolder The ViewHolder after the layout is finished, might be the same + * instance with oldHolder. + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateChange(@NonNull ViewHolder oldHolder, + @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + @AdapterChanges static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) { + int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED); + if (viewHolder.isInvalid()) { + return FLAG_INVALIDATED; + } + if ((flags & FLAG_INVALIDATED) == 0) { + final int oldPos = viewHolder.getOldPosition(); + final int pos = viewHolder.getAdapterPosition(); + if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) { + flags |= FLAG_MOVED; + } + } + return flags; + } + + /** + * Called when there are pending animations waiting to be started. This state + * is governed by the return values from + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be + * called later to start the associated animations. runPendingAnimations() will be scheduled + * to be run on the next frame. + */ + public abstract void runPendingAnimations(); + + /** + * Method called when an animation on a view should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on the item + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + * + * @param item The item for which an animation should be stopped. + */ + public abstract void endAnimation(ViewHolder item); + + /** + * Method called when all item animations should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on any items + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + */ + public abstract void endAnimations(); + + /** + * Method which returns whether there are any item animations currently running. + * This method can be used to determine whether to delay other actions until + * animations end. + * + * @return true if there are any item animations currently running, false otherwise. + */ + public abstract boolean isRunning(); + + /** + * Method to be called by subclasses when an animation is finished. + * <p> + * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there + * should + * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass. + * <p> + * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the <code>oldHolder</code> + * and <code>newHolder</code> (if they are not the same instance). + * + * @param viewHolder The ViewHolder whose animation is finished. + * @see #onAnimationFinished(ViewHolder) + */ + public final void dispatchAnimationFinished(ViewHolder viewHolder) { + onAnimationFinished(viewHolder); + if (mListener != null) { + mListener.onAnimationFinished(viewHolder); + } + } + + /** + * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the + * ItemAnimator. + * + * @param viewHolder The ViewHolder whose animation is finished. There might still be other + * animations running on this ViewHolder. + * @see #dispatchAnimationFinished(ViewHolder) + */ + public void onAnimationFinished(ViewHolder viewHolder) { + } + + /** + * Method to be called by subclasses when an animation is started. + * <p> + * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there should be a matching + * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass. + * <p> + * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the <code>oldHolder</code> + * and <code>newHolder</code> (if they are not the same instance). + * <p> + * If your ItemAnimator decides not to animate a ViewHolder, it should call + * {@link #dispatchAnimationFinished(ViewHolder)} <b>without</b> calling + * {@link #dispatchAnimationStarted(ViewHolder)}. + * + * @param viewHolder The ViewHolder whose animation is starting. + * @see #onAnimationStarted(ViewHolder) + */ + public final void dispatchAnimationStarted(ViewHolder viewHolder) { + onAnimationStarted(viewHolder); + } + + /** + * Called when a new animation is started on the given ViewHolder. + * + * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder + * might already be animating and this might be another animation. + * @see #dispatchAnimationStarted(ViewHolder) + */ + public void onAnimationStarted(ViewHolder viewHolder) { + + } + + /** + * Like {@link #isRunning()}, this method returns whether there are any item + * animations currently running. Additionally, the listener passed in will be called + * when there are no item animations running, either immediately (before the method + * returns) if no animations are currently running, or when the currently running + * animations are {@link #dispatchAnimationsFinished() finished}. + * + * <p>Note that the listener is transient - it is either called immediately and not + * stored at all, or stored only until it is called when running animations + * are finished sometime later.</p> + * + * @param listener A listener to be called immediately if no animations are running + * or later when currently-running animations have finished. A null listener is + * equivalent to calling {@link #isRunning()}. + * @return true if there are any item animations currently running, false otherwise. + */ + public final boolean isRunning(ItemAnimatorFinishedListener listener) { + boolean running = isRunning(); + if (listener != null) { + if (!running) { + listener.onAnimationsFinished(); + } else { + mFinishedListeners.add(listener); + } + } + return running; + } + + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + * <p> + * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + * <p> + * If your application is using change payloads, you can override + * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation returns <code>true</code>. + * + * @see #canReuseUpdatedViewHolder(ViewHolder, List) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) { + return true; + } + + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + * <p> + * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * @param payloads A non-null list of merged payloads that were sent with change + * notifications. Can be empty if the adapter is invalidated via + * {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of + * payloads will be passed into + * {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)} + * method <b>if</b> this method returns <code>true</code>. + * + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation calls + * {@link #canReuseUpdatedViewHolder(ViewHolder)}. + * + * @see #canReuseUpdatedViewHolder(ViewHolder) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List<Object> payloads) { + return canReuseUpdatedViewHolder(viewHolder); + } + + /** + * This method should be called by ItemAnimator implementations to notify + * any listeners that all pending and active item animations are finished. + */ + public final void dispatchAnimationsFinished() { + final int count = mFinishedListeners.size(); + for (int i = 0; i < count; ++i) { + mFinishedListeners.get(i).onAnimationsFinished(); + } + mFinishedListeners.clear(); + } + + /** + * Returns a new {@link ItemHolderInfo} which will be used to store information about the + * ViewHolder. This information will later be passed into <code>animate**</code> methods. + * <p> + * You can override this method if you want to extend {@link ItemHolderInfo} and provide + * your own instances. + * + * @return A new {@link ItemHolderInfo}. + */ + public ItemHolderInfo obtainHolderInfo() { + return new ItemHolderInfo(); + } + + /** + * The interface to be implemented by listeners to animation events from this + * ItemAnimator. This is used internally and is not intended for developers to + * create directly. + */ + interface ItemAnimatorListener { + void onAnimationFinished(ViewHolder item); + } + + /** + * This interface is used to inform listeners when all pending or running animations + * in an ItemAnimator are finished. This can be used, for example, to delay an action + * in a data set until currently-running animations are complete. + * + * @see #isRunning(ItemAnimatorFinishedListener) + */ + public interface ItemAnimatorFinishedListener { + /** + * Notifies when all pending or running animations in an ItemAnimator are finished. + */ + void onAnimationsFinished(); + } + + /** + * A simple data structure that holds information about an item's bounds. + * This information is used in calculating item animations. Default implementation of + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and + * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data + * structure. You can extend this class if you would like to keep more information about + * the Views. + * <p> + * If you want to provide your own implementation but still use `super` methods to record + * basic information, you can override {@link #obtainHolderInfo()} to provide your own + * instances. + */ + public static class ItemHolderInfo { + + /** + * The left edge of the View (excluding decorations) + */ + public int left; + + /** + * The top edge of the View (excluding decorations) + */ + public int top; + + /** + * The right edge of the View (excluding decorations) + */ + public int right; + + /** + * The bottom edge of the View (excluding decorations) + */ + public int bottom; + + /** + * The change flags that were passed to + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}. + */ + @AdapterChanges + public int changeFlags; + + public ItemHolderInfo() { + } + + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder. Clears all {@link #changeFlags}. + * + * @param holder The ViewHolder whose bounds should be copied. + * @return This {@link ItemHolderInfo} + */ + public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) { + return setFrom(holder, 0); + } + + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter. + * + * @param holder The ViewHolder whose bounds should be copied. + * @param flags The adapter change flags that were passed into + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, + * List)}. + * @return This {@link ItemHolderInfo} + */ + public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder, + @AdapterChanges int flags) { + final View view = holder.itemView; + this.left = view.getLeft(); + this.top = view.getTop(); + this.right = view.getRight(); + this.bottom = view.getBottom(); + return this; + } + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderCallback == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); + } + } + + /** + * A callback interface that can be used to alter the drawing order of RecyclerView children. + * <p> + * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case + * that applies to that method also applies to this callback. For example, changing the drawing + * order of two views will not have any effect if their elevation values are different since + * elevation overrides the result of this callback. + */ + public interface ChildDrawingOrderCallback { + /** + * Returns the index of the child to draw for this iteration. Override this + * if you want to change the drawing order of children. By default, it + * returns i. + * + * @param i The current iteration. + * @return The index of the child to draw this iteration. + * + * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) + */ + int onGetChildDrawingOrder(int childCount, int i); + } +} diff --git a/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java b/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java new file mode 100644 index 000000000000..282da644ac8b --- /dev/null +++ b/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.os.Bundle; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * The AccessibilityDelegate used by RecyclerView. + * <p> + * This class handles basic accessibility actions and delegates them to LayoutManager. + */ +public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegate { + final RecyclerView mRecyclerView; + + + public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + } + + boolean shouldIgnore() { + return mRecyclerView.hasPendingAdapterUpdates(); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args); + } + + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(RecyclerView.class.getName()); + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info); + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(RecyclerView.class.getName()); + if (host instanceof RecyclerView && !shouldIgnore()) { + RecyclerView rv = (RecyclerView) host; + if (rv.getLayoutManager() != null) { + rv.getLayoutManager().onInitializeAccessibilityEvent(event); + } + } + } + + /** + * Gets the AccessibilityDelegate for an individual item in the RecyclerView. + * A basic item delegate is provided by default, but you can override this + * method to provide a custom per-item delegate. + */ + public AccessibilityDelegate getItemDelegate() { + return mItemDelegate; + } + + final AccessibilityDelegate mItemDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + mRecyclerView.getLayoutManager() + .onInitializeAccessibilityNodeInfoForItem(host, info); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + return mRecyclerView.getLayoutManager() + .performAccessibilityActionForItem(host, action, args); + } + return false; + } + }; +} + diff --git a/core/java/com/android/internal/widget/ScrollbarHelper.java b/core/java/com/android/internal/widget/ScrollbarHelper.java new file mode 100644 index 000000000000..ae34e4c558d6 --- /dev/null +++ b/core/java/com/android/internal/widget/ScrollbarHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.view.View; + +/** + * A helper class to do scroll offset calculations. + */ +class ScrollbarHelper { + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled, boolean reverseLayout) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + final int minPosition = Math.min(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int maxPosition = Math.max(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int itemsBefore = reverseLayout + ? Math.max(0, state.getItemCount() - maxPosition - 1) + : Math.max(0, minPosition); + if (!smoothScrollbarEnabled) { + return itemsBefore; + } + final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild)); + final int itemRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + 1; + final float avgSizePerRow = (float) laidOutArea / itemRange; + + return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding() + - orientation.getDecoratedStart(startChild))); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; + } + final int extend = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + return Math.min(orientation.getTotalSpace(), extend); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return state.getItemCount(); + } + // smooth scrollbar enabled. try to estimate better. + final int laidOutArea = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + final int laidOutRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + + 1; + // estimate a size for full list. + return (int) ((float) laidOutArea / laidOutRange * state.getItemCount()); + } +} diff --git a/core/java/com/android/internal/widget/ScrollingView.java b/core/java/com/android/internal/widget/ScrollingView.java new file mode 100644 index 000000000000..a0205e7771e7 --- /dev/null +++ b/core/java/com/android/internal/widget/ScrollingView.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +/** + * An interface that can be implemented by Views to provide scroll related APIs. + */ +public interface ScrollingView { + /** + * <p>Compute the horizontal range that the horizontal scrollbar + * represents.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollExtent()} and + * {@link #computeHorizontalScrollOffset()}.</p> + * + * <p>The default range is the drawing width of this view.</p> + * + * @return the total horizontal range represented by the horizontal + * scrollbar + * + * @see #computeHorizontalScrollExtent() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollRange(); + + /** + * <p>Compute the horizontal offset of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollExtent()}.</p> + * + * <p>The default offset is the scroll offset of this view.</p> + * + * @return the horizontal offset of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollOffset(); + + /** + * <p>Compute the horizontal extent of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the length + * of the thumb within the scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollOffset()}.</p> + * + * <p>The default extent is the drawing width of this view.</p> + * + * @return the horizontal extent of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollExtent(); + + /** + * <p>Compute the vertical range that the vertical scrollbar represents.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollExtent()} and + * {@link #computeVerticalScrollOffset()}.</p> + * + * @return the total vertical range represented by the vertical scrollbar + * + * <p>The default range is the drawing height of this view.</p> + * + * @see #computeVerticalScrollExtent() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollRange(); + + /** + * <p>Compute the vertical offset of the vertical scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollExtent()}.</p> + * + * <p>The default offset is the scroll offset of this view.</p> + * + * @return the vertical offset of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollOffset(); + + /** + * <p>Compute the vertical extent of the vertical scrollbar's thumb + * within the vertical range. This value is used to compute the length + * of the thumb within the scrollbar's track.</p> + * + * <p>The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollOffset()}.</p> + * + * <p>The default extent is the drawing height of this view.</p> + * + * @return the vertical extent of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollExtent(); +} diff --git a/core/java/com/android/internal/widget/SimpleItemAnimator.java b/core/java/com/android/internal/widget/SimpleItemAnimator.java new file mode 100644 index 000000000000..f4cc7536ba4b --- /dev/null +++ b/core/java/com/android/internal/widget/SimpleItemAnimator.java @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Log; +import android.view.View; + +import com.android.internal.widget.RecyclerView.Adapter; +import com.android.internal.widget.RecyclerView.ViewHolder; + +/** + * A wrapper class for ItemAnimator that records View bounds and decides whether it should run + * move, change, add or remove animations. This class also replicates the original ItemAnimator + * API. + * <p> + * It uses {@link ItemHolderInfo} to track the bounds information of the Views. If you would like + * to + * extend this class, you can override {@link #obtainHolderInfo()} method to provide your own info + * class that extends {@link ItemHolderInfo}. + */ +public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator { + + private static final boolean DEBUG = false; + + private static final String TAG = "SimpleItemAnimator"; + + boolean mSupportsChangeAnimations = true; + + /** + * Returns whether this ItemAnimator supports animations of change events. + * + * @return true if change animations are supported, false otherwise + */ + @SuppressWarnings("unused") + public boolean getSupportsChangeAnimations() { + return mSupportsChangeAnimations; + } + + /** + * Sets whether this ItemAnimator supports animations of item change events. + * If you set this property to false, actions on the data set which change the + * contents of items will not be animated. What those animations do is left + * up to the discretion of the ItemAnimator subclass, in its + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} implementation. + * The value of this property is true by default. + * + * @param supportsChangeAnimations true if change animations are supported by + * this ItemAnimator, false otherwise. If the property is false, + * the ItemAnimator + * will not receive a call to + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, + * int)} when changes occur. + * @see Adapter#notifyItemChanged(int) + * @see Adapter#notifyItemRangeChanged(int, int) + */ + public void setSupportsChangeAnimations(boolean supportsChangeAnimations) { + mSupportsChangeAnimations = supportsChangeAnimations; + } + + /** + * {@inheritDoc} + * + * @return True if change animations are not supported or the ViewHolder is invalid, + * false otherwise. + * + * @see #setSupportsChangeAnimations(boolean) + */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { + return !mSupportsChangeAnimations || viewHolder.isInvalid(); + } + + @Override + public boolean animateDisappearance(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + int oldLeft = preLayoutInfo.left; + int oldTop = preLayoutInfo.top; + View disappearingItemView = viewHolder.itemView; + int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left; + int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top; + if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) { + disappearingItemView.layout(newLeft, newTop, + newLeft + disappearingItemView.getWidth(), + newTop + disappearingItemView.getHeight()); + if (DEBUG) { + Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView); + } + return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop); + } else { + if (DEBUG) { + Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView); + } + return animateRemove(viewHolder); + } + } + + @Override + public boolean animateAppearance(@NonNull ViewHolder viewHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left + || preLayoutInfo.top != postLayoutInfo.top)) { + // slide items in if before/after locations differ + if (DEBUG) { + Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder); + } + return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top, + postLayoutInfo.left, postLayoutInfo.top); + } else { + if (DEBUG) { + Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder); + } + return animateAdd(viewHolder); + } + } + + @Override + public boolean animatePersistence(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) { + if (DEBUG) { + Log.d(TAG, "PERSISTENT: " + viewHolder + + " with view " + viewHolder.itemView); + } + return animateMove(viewHolder, + preInfo.left, preInfo.top, postInfo.left, postInfo.top); + } + dispatchMoveFinished(viewHolder); + return false; + } + + @Override + public boolean animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + if (DEBUG) { + Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView); + } + final int fromLeft = preInfo.left; + final int fromTop = preInfo.top; + final int toLeft, toTop; + if (newHolder.shouldIgnore()) { + toLeft = preInfo.left; + toTop = preInfo.top; + } else { + toLeft = postInfo.left; + toTop = postInfo.top; + } + return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop); + } + + /** + * Called when an item is removed from the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchRemoveFinished(ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(ViewHolder) animateRemove()}, and + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * <p>This method may also be called for disappearing items which continue to exist in the + * RecyclerView, but for which the system does not have enough information to animate + * them out of view. In that case, the default animation for removing items is run + * on those items as well.</p> + * + * @param holder The item that is being removed. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateRemove(ViewHolder holder); + + /** + * Called when an item is added to the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchAddFinished(ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(ViewHolder) animateRemove()}, and + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * <p>This method may also be called for appearing items which were already in the + * RecyclerView, but for which the system does not have enough information to animate + * them into view. In that case, the default animation for adding items is run + * on those items as well.</p> + * + * @param holder The item that is being added. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateAdd(ViewHolder holder); + + /** + * Called when an item is moved in the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchMoveFinished(ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(ViewHolder) animateRemove()}, and + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * @param holder The item that is being moved. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateMove(ViewHolder holder, int fromX, int fromY, + int toX, int toY); + + /** + * Called when an item is changed in the RecyclerView, as indicated by a call to + * {@link Adapter#notifyItemChanged(int)} or + * {@link Adapter#notifyItemRangeChanged(int, int)}. + * <p> + * Implementers can choose whether and how to animate changes, but must always call + * {@link #dispatchChangeFinished(ViewHolder, boolean)} for each non-null distinct ViewHolder, + * either immediately (if no animation will occur) or after the animation actually finishes. + * If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call + * {@link #dispatchChangeFinished(ViewHolder, boolean)} once and only once. In that case, the + * second parameter of {@code dispatchChangeFinished} is ignored. + * <p> + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(ViewHolder) animateRemove()}, and + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * @param oldHolder The original item that changed. + * @param newHolder The new item that was created with the changed content. Might be null + * @param fromLeft Left of the old view holder + * @param fromTop Top of the old view holder + * @param toLeft Left of the new view holder + * @param toTop Top of the new view holder + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateChange(ViewHolder oldHolder, + ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop); + + /** + * Method to be called by subclasses when a remove animation is done. + * + * @param item The item which has been removed + * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + */ + public final void dispatchRemoveFinished(ViewHolder item) { + onRemoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a move animation is done. + * + * @param item The item which has been moved + * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public final void dispatchMoveFinished(ViewHolder item) { + onMoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when an add animation is done. + * + * @param item The item which has been added + */ + public final void dispatchAddFinished(ViewHolder item) { + onAddFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a change animation is done. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + * @see #animateChange(ViewHolder, ViewHolder, int, int, int, int) + */ + public final void dispatchChangeFinished(ViewHolder item, boolean oldItem) { + onChangeFinished(item, oldItem); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a remove animation is being started. + * + * @param item The item being removed + */ + public final void dispatchRemoveStarting(ViewHolder item) { + onRemoveStarting(item); + } + + /** + * Method to be called by subclasses when a move animation is being started. + * + * @param item The item being moved + */ + public final void dispatchMoveStarting(ViewHolder item) { + onMoveStarting(item); + } + + /** + * Method to be called by subclasses when an add animation is being started. + * + * @param item The item being added + */ + public final void dispatchAddStarting(ViewHolder item) { + onAddStarting(item); + } + + /** + * Method to be called by subclasses when a change animation is being started. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + public final void dispatchChangeStarting(ViewHolder item, boolean oldItem) { + onChangeStarting(item, oldItem); + } + + /** + * Called when a remove animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onRemoveStarting(ViewHolder item) { + } + + /** + * Called when a remove animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onRemoveFinished(ViewHolder item) { + } + + /** + * Called when an add animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onAddStarting(ViewHolder item) { + } + + /** + * Called when an add animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onAddFinished(ViewHolder item) { + } + + /** + * Called when a move animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onMoveStarting(ViewHolder item) { + } + + /** + * Called when a move animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onMoveFinished(ViewHolder item) { + } + + /** + * Called when a change animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + @SuppressWarnings("UnusedParameters") + public void onChangeStarting(ViewHolder item, boolean oldItem) { + } + + /** + * Called when a change animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + public void onChangeFinished(ViewHolder item, boolean oldItem) { + } +} + diff --git a/core/java/com/android/internal/widget/ViewInfoStore.java b/core/java/com/android/internal/widget/ViewInfoStore.java new file mode 100644 index 000000000000..6784a853d0d1 --- /dev/null +++ b/core/java/com/android/internal/widget/ViewInfoStore.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import android.util.Pools; + +import static com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo; +import static com.android.internal.widget.RecyclerView.ViewHolder; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_POST; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * This class abstracts all tracking for Views to run animations. + */ +class ViewInfoStore { + + private static final boolean DEBUG = false; + + /** + * View data records for pre-layout + */ + @VisibleForTesting + final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>(); + + @VisibleForTesting + final LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>(); + + /** + * Clears the state and all existing tracking data + */ + void clear() { + mLayoutHolderMap.clear(); + mOldChangedHolders.clear(); + } + + /** + * Adds the item information to the prelayout tracking + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPreLayout(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.preInfo = info; + record.flags |= FLAG_PRE; + } + + boolean isDisappearing(ViewHolder holder) { + final InfoRecord record = mLayoutHolderMap.get(holder); + return record != null && ((record.flags & FLAG_DISAPPEARED) != 0); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + ItemHolderInfo popFromPreLayout(ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_PRE); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + ItemHolderInfo popFromPostLayout(ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_POST); + } + + private ItemHolderInfo popFromLayoutStep(ViewHolder vh, int flag) { + int index = mLayoutHolderMap.indexOfKey(vh); + if (index < 0) { + return null; + } + final InfoRecord record = mLayoutHolderMap.valueAt(index); + if (record != null && (record.flags & flag) != 0) { + record.flags &= ~flag; + final ItemHolderInfo info; + if (flag == FLAG_PRE) { + info = record.preInfo; + } else if (flag == FLAG_POST) { + info = record.postInfo; + } else { + throw new IllegalArgumentException("Must provide flag PRE or POST"); + } + // if not pre-post flag is left, clear. + if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) { + mLayoutHolderMap.removeAt(index); + InfoRecord.recycle(record); + } + return info; + } + return null; + } + + /** + * Adds the given ViewHolder to the oldChangeHolders list + * @param key The key to identify the ViewHolder. + * @param holder The ViewHolder to store + */ + void addToOldChangeHolders(long key, ViewHolder holder) { + mOldChangedHolders.put(key, holder); + } + + /** + * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the + * LayoutManager during a pre-layout pass. We distinguish them from other views that were + * already in the pre-layout so that ItemAnimator can choose to run a different animation for + * them. + * + * @param holder The ViewHolder to store + * @param info The information to save + */ + void addToAppearedInPreLayoutHolders(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_APPEAR; + record.preInfo = info; + } + + /** + * Checks whether the given ViewHolder is in preLayout list + * @param viewHolder The ViewHolder to query + * + * @return True if the ViewHolder is present in preLayout, false otherwise + */ + boolean isInPreLayout(ViewHolder viewHolder) { + final InfoRecord record = mLayoutHolderMap.get(viewHolder); + return record != null && (record.flags & FLAG_PRE) != 0; + } + + /** + * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns + * null. + * @param key The key to be used to find the ViewHolder. + * + * @return A ViewHolder if exists or null if it does not exist. + */ + ViewHolder getFromOldChangeHolders(long key) { + return mOldChangedHolders.get(key); + } + + /** + * Adds the item information to the post layout list + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPostLayout(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.postInfo = info; + record.flags |= FLAG_POST; + } + + /** + * A ViewHolder might be added by the LayoutManager just to animate its disappearance. + * This list holds such items so that we can animate / recycle these ViewHolders properly. + * + * @param holder The ViewHolder which disappeared during a layout. + */ + void addToDisappearedInLayout(ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_DISAPPEARED; + } + + /** + * Removes a ViewHolder from disappearing list. + * @param holder The ViewHolder to be removed from the disappearing list. + */ + void removeFromDisappearedInLayout(ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + return; + } + record.flags &= ~FLAG_DISAPPEARED; + } + + void process(ProcessCallback callback) { + for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { + final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); + final InfoRecord record = mLayoutHolderMap.removeAt(index); + if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { + // Appeared then disappeared. Not useful for animations. + callback.unused(viewHolder); + } else if ((record.flags & FLAG_DISAPPEARED) != 0) { + // Set as "disappeared" by the LayoutManager (addDisappearingView) + if (record.preInfo == null) { + // similar to appear disappear but happened between different layout passes. + // this can happen when the layout manager is using auto-measure + callback.unused(viewHolder); + } else { + callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); + } + } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { + // Appeared in the layout but not in the adapter (e.g. entered the viewport) + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { + // Persistent in both passes. Animate persistence + callback.processPersistent(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE) != 0) { + // Was in pre-layout, never been added to post layout + callback.processDisappeared(viewHolder, record.preInfo, null); + } else if ((record.flags & FLAG_POST) != 0) { + // Was not in pre-layout, been added to post layout + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_APPEAR) != 0) { + // Scrap view. RecyclerView will handle removing/recycling this. + } else if (DEBUG) { + throw new IllegalStateException("record without any reasonable flag combination:/"); + } + InfoRecord.recycle(record); + } + } + + /** + * Removes the ViewHolder from all list + * @param holder The ViewHolder which we should stop tracking + */ + void removeViewHolder(ViewHolder holder) { + for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) { + if (holder == mOldChangedHolders.valueAt(i)) { + mOldChangedHolders.removeAt(i); + break; + } + } + final InfoRecord info = mLayoutHolderMap.remove(holder); + if (info != null) { + InfoRecord.recycle(info); + } + } + + void onDetach() { + InfoRecord.drainCache(); + } + + public void onViewDetached(ViewHolder viewHolder) { + removeFromDisappearedInLayout(viewHolder); + } + + interface ProcessCallback { + void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, + @Nullable ItemHolderInfo postInfo); + void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo, + ItemHolderInfo postInfo); + void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, + @NonNull ItemHolderInfo postInfo); + void unused(ViewHolder holder); + } + + static class InfoRecord { + // disappearing list + static final int FLAG_DISAPPEARED = 1; + // appear in pre layout list + static final int FLAG_APPEAR = 1 << 1; + // pre layout, this is necessary to distinguish null item info + static final int FLAG_PRE = 1 << 2; + // post layout, this is necessary to distinguish null item info + static final int FLAG_POST = 1 << 3; + static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; + static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; + static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; + int flags; + @Nullable ItemHolderInfo preInfo; + @Nullable ItemHolderInfo postInfo; + static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20); + + private InfoRecord() { + } + + static InfoRecord obtain() { + InfoRecord record = sPool.acquire(); + return record == null ? new InfoRecord() : record; + } + + static void recycle(InfoRecord record) { + record.flags = 0; + record.preInfo = null; + record.postInfo = null; + sPool.release(record); + } + + static void drainCache() { + //noinspection StatementWithEmptyBody + while (sPool.acquire() != null); + } + } +} diff --git a/core/java/com/android/internal/widget/helper/ItemTouchHelper.java b/core/java/com/android/internal/widget/helper/ItemTouchHelper.java new file mode 100644 index 000000000000..9636ed87696a --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchHelper.java @@ -0,0 +1,2391 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget.helper; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.animation.Interpolator; + +import com.android.internal.R; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; +import com.android.internal.widget.RecyclerView.OnItemTouchListener; +import com.android.internal.widget.RecyclerView.ViewHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. + * <p> + * It works with a RecyclerView and a Callback class, which configures what type of interactions + * are enabled and also receives events when user performs these actions. + * <p> + * Depending on which functionality you support, you should override + * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or + * {@link Callback#onSwiped(ViewHolder, int)}. + * <p> + * This class is designed to work with any LayoutManager but for certain situations, it can be + * optimized for your custom LayoutManager by extending methods in the + * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler} + * interface in your LayoutManager. + * <p> + * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. On + * platforms older than Honeycomb, ItemTouchHelper uses canvas translations and View's visibility + * property to move items in response to touch events. You can customize these behaviors by + * overriding {@link Callback#onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean)} + * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean)}. + * <p/> + * Most of the time, you only need to override <code>onChildDraw</code> but due to limitations of + * platform prior to Honeycomb, you may need to implement <code>onChildDrawOver</code> as well. + */ +public class ItemTouchHelper extends RecyclerView.ItemDecoration + implements RecyclerView.OnChildAttachStateChangeListener { + + /** + * Up direction, used for swipe & drag control. + */ + public static final int UP = 1; + + /** + * Down direction, used for swipe & drag control. + */ + public static final int DOWN = 1 << 1; + + /** + * Left direction, used for swipe & drag control. + */ + public static final int LEFT = 1 << 2; + + /** + * Right direction, used for swipe & drag control. + */ + public static final int RIGHT = 1 << 3; + + // If you change these relative direction values, update Callback#convertToAbsoluteDirection, + // Callback#convertToRelativeDirection. + /** + * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout + * direction. Used for swipe & drag control. + */ + public static final int START = LEFT << 2; + + /** + * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout + * direction. Used for swipe & drag control. + */ + public static final int END = RIGHT << 2; + + /** + * ItemTouchHelper is in idle state. At this state, either there is no related motion event by + * the user or latest motion events have not yet triggered a swipe or drag. + */ + public static final int ACTION_STATE_IDLE = 0; + + /** + * A View is currently being swiped. + */ + public static final int ACTION_STATE_SWIPE = 1; + + /** + * A View is currently being dragged. + */ + public static final int ACTION_STATE_DRAG = 2; + + /** + * Animation type for views which are swiped successfully. + */ + public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; + + /** + * Animation type for views which are not completely swiped thus will animate back to their + * original position. + */ + public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; + + /** + * Animation type for views that were dragged and now will animate to their final position. + */ + public static final int ANIMATION_TYPE_DRAG = 1 << 3; + + static final String TAG = "ItemTouchHelper"; + + static final boolean DEBUG = false; + + static final int ACTIVE_POINTER_ID_NONE = -1; + + static final int DIRECTION_FLAG_COUNT = 8; + + private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; + + static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; + + static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; + + /** + * The unit we are using to track velocity + */ + private static final int PIXELS_PER_SECOND = 1000; + + /** + * Views, whose state should be cleared after they are detached from RecyclerView. + * This is necessary after swipe dismissing an item. We wait until animator finishes its job + * to clean these views. + */ + final List<View> mPendingCleanup = new ArrayList<View>(); + + /** + * Re-use array to calculate dx dy for a ViewHolder + */ + private final float[] mTmpPosition = new float[2]; + + /** + * Currently selected view holder + */ + ViewHolder mSelected = null; + + /** + * The reference coordinates for the action start. For drag & drop, this is the time long + * press is completed vs for swipe, this is the initial touch point. + */ + float mInitialTouchX; + + float mInitialTouchY; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + float mSwipeEscapeVelocity; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + float mMaxSwipeVelocity; + + /** + * The diff between the last event and initial touch. + */ + float mDx; + + float mDy; + + /** + * The coordinates of the selected view at the time it is selected. We record these values + * when action starts so that we can consistently position it even if LayoutManager moves the + * View. + */ + float mSelectedStartX; + + float mSelectedStartY; + + /** + * The pointer we are tracking. + */ + int mActivePointerId = ACTIVE_POINTER_ID_NONE; + + /** + * Developer callback which controls the behavior of ItemTouchHelper. + */ + Callback mCallback; + + /** + * Current mode. + */ + int mActionState = ACTION_STATE_IDLE; + + /** + * The direction flags obtained from unmasking + * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current + * action state. + */ + int mSelectedFlags; + + /** + * When a View is dragged or swiped and needs to go back to where it was, we create a Recover + * Animation and animate it to its location using this custom Animator, instead of using + * framework Animators. + * Using framework animators has the side effect of clashing with ItemAnimator, creating + * jumpy UIs. + */ + List<RecoverAnimation> mRecoverAnimations = new ArrayList<RecoverAnimation>(); + + private int mSlop; + + RecyclerView mRecyclerView; + + /** + * When user drags a view to the edge, we start scrolling the LayoutManager as long as View + * is partially out of bounds. + */ + final Runnable mScrollRunnable = new Runnable() { + @Override + public void run() { + if (mSelected != null && scrollIfNecessary()) { + if (mSelected != null) { //it might be lost during scrolling + moveIfNecessary(mSelected); + } + mRecyclerView.removeCallbacks(mScrollRunnable); + mRecyclerView.postOnAnimation(this); + } + } + }; + + /** + * Used for detecting fling swipe + */ + VelocityTracker mVelocityTracker; + + //re-used list for selecting a swap target + private List<ViewHolder> mSwapTargets; + + //re used for for sorting swap targets + private List<Integer> mDistances; + + /** + * If drag & drop is supported, we use child drawing order to bring them to front. + */ + private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; + + /** + * This keeps a reference to the child dragged by the user. Even after user stops dragging, + * until view reaches its final position (end of recover animation), we keep a reference so + * that it can be drawn above other children. + */ + View mOverdrawChild = null; + + /** + * We cache the position of the overdraw child to avoid recalculating it each time child + * position callback is called. This value is invalidated whenever a child is attached or + * detached. + */ + int mOverdrawChildPosition = -1; + + /** + * Used to detect long press. + */ + GestureDetector mGestureDetector; + + private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); + } + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mActivePointerId = event.getPointerId(0); + mInitialTouchX = event.getX(); + mInitialTouchY = event.getY(); + obtainVelocityTracker(); + if (mSelected == null) { + final RecoverAnimation animation = findAnimation(event); + if (animation != null) { + mInitialTouchX -= animation.mX; + mInitialTouchY -= animation.mY; + endRecoverAnimation(animation.mViewHolder, true); + if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { + mCallback.clearView(mRecyclerView, animation.mViewHolder); + } + select(animation.mViewHolder, animation.mActionState); + updateDxDy(event, mSelectedFlags, 0); + } + } + } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mActivePointerId = ACTIVE_POINTER_ID_NONE; + select(null, ACTION_STATE_IDLE); + } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { + // in a non scroll orientation, if distance change is above threshold, we + // can select the item + final int index = event.findPointerIndex(mActivePointerId); + if (DEBUG) { + Log.d(TAG, "pointer index " + index); + } + if (index >= 0) { + checkSelectForSwipe(action, event, index); + } + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + return mSelected != null; + } + + @Override + public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, + "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { + return; + } + final int action = event.getActionMasked(); + final int activePointerIndex = event.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + checkSelectForSwipe(action, event, activePointerIndex); + } + ViewHolder viewHolder = mSelected; + if (viewHolder == null) { + return; + } + switch (action) { + case MotionEvent.ACTION_MOVE: { + // Find the index of the active pointer and fetch its position + if (activePointerIndex >= 0) { + updateDxDy(event, mSelectedFlags, activePointerIndex); + moveIfNecessary(viewHolder); + mRecyclerView.removeCallbacks(mScrollRunnable); + mScrollRunnable.run(); + mRecyclerView.invalidate(); + } + break; + } + case MotionEvent.ACTION_CANCEL: + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + // fall through + case MotionEvent.ACTION_UP: + select(null, ACTION_STATE_IDLE); + mActivePointerId = ACTIVE_POINTER_ID_NONE; + break; + case MotionEvent.ACTION_POINTER_UP: { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = event.getPointerId(newPointerIndex); + updateDxDy(event, mSelectedFlags, pointerIndex); + } + break; + } + } + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (!disallowIntercept) { + return; + } + select(null, ACTION_STATE_IDLE); + } + }; + + /** + * Temporary rect instance that is used when we need to lookup Item decorations. + */ + private Rect mTmpRect; + + /** + * When user started to drag scroll. Reset when we don't scroll + */ + private long mDragScrollStartTimeInMs; + + /** + * Creates an ItemTouchHelper that will work with the given Callback. + * <p> + * You can attach ItemTouchHelper to a RecyclerView via + * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, + * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. + * + * @param callback The Callback which controls the behavior of this touch helper. + */ + public ItemTouchHelper(Callback callback) { + mCallback = callback; + } + + private static boolean hitTest(View child, float x, float y, float left, float top) { + return x >= left + && x <= left + child.getWidth() + && y >= top + && y <= top + child.getHeight(); + } + + /** + * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already + * attached to a RecyclerView, it will first detach from the previous one. You can call this + * method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove ItemTouchHelper from the current + * RecyclerView. + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + final Resources resources = recyclerView.getResources(); + mSwipeEscapeVelocity = resources + .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); + mMaxSwipeVelocity = resources + .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); + setupCallbacks(); + } + } + + private void setupCallbacks() { + ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); + mSlop = vc.getScaledTouchSlop(); + mRecyclerView.addItemDecoration(this); + mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); + mRecyclerView.addOnChildAttachStateChangeListener(this); + initGestureDetector(); + } + + private void destroyCallbacks() { + mRecyclerView.removeItemDecoration(this); + mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); + mRecyclerView.removeOnChildAttachStateChangeListener(this); + // clean all attached + final int recoverAnimSize = mRecoverAnimations.size(); + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); + mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); + } + mRecoverAnimations.clear(); + mOverdrawChild = null; + mOverdrawChildPosition = -1; + releaseVelocityTracker(); + } + + private void initGestureDetector() { + if (mGestureDetector != null) { + return; + } + mGestureDetector = new GestureDetector(mRecyclerView.getContext(), + new ItemTouchHelperGestureListener()); + } + + private void getSelectedDxDy(float[] outPosition) { + if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { + outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); + } else { + outPosition[0] = mSelected.itemView.getTranslationX(); + } + if ((mSelectedFlags & (UP | DOWN)) != 0) { + outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); + } else { + outPosition[1] = mSelected.itemView.getTranslationY(); + } + } + + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + float dx = 0, dy = 0; + if (mSelected != null) { + getSelectedDxDy(mTmpPosition); + dx = mTmpPosition[0]; + dy = mTmpPosition[1]; + } + mCallback.onDrawOver(c, parent, mSelected, + mRecoverAnimations, mActionState, dx, dy); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + // we don't know if RV changed something so we should invalidate this index. + mOverdrawChildPosition = -1; + float dx = 0, dy = 0; + if (mSelected != null) { + getSelectedDxDy(mTmpPosition); + dx = mTmpPosition[0]; + dy = mTmpPosition[1]; + } + mCallback.onDraw(c, parent, mSelected, + mRecoverAnimations, mActionState, dx, dy); + } + + /** + * Starts dragging or swiping the given View. Call with null if you want to clear it. + * + * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the + * current action + * @param actionState The type of action + */ + void select(ViewHolder selected, int actionState) { + if (selected == mSelected && actionState == mActionState) { + return; + } + mDragScrollStartTimeInMs = Long.MIN_VALUE; + final int prevActionState = mActionState; + // prevent duplicate animations + endRecoverAnimation(selected, true); + mActionState = actionState; + if (actionState == ACTION_STATE_DRAG) { + // we remove after animation is complete. this means we only elevate the last drag + // child but that should perform good enough as it is very hard to start dragging a + // new child before the previous one settles. + mOverdrawChild = selected.itemView; + addChildDrawingOrderCallback(); + } + int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) + - 1; + boolean preventLayout = false; + + if (mSelected != null) { + final ViewHolder prevSelected = mSelected; + if (prevSelected.itemView.getParent() != null) { + final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 + : swipeIfNecessary(prevSelected); + releaseVelocityTracker(); + // find where we should animate to + final float targetTranslateX, targetTranslateY; + int animationType; + switch (swipeDir) { + case LEFT: + case RIGHT: + case START: + case END: + targetTranslateY = 0; + targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); + break; + case UP: + case DOWN: + targetTranslateX = 0; + targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); + break; + default: + targetTranslateX = 0; + targetTranslateY = 0; + } + if (prevActionState == ACTION_STATE_DRAG) { + animationType = ANIMATION_TYPE_DRAG; + } else if (swipeDir > 0) { + animationType = ANIMATION_TYPE_SWIPE_SUCCESS; + } else { + animationType = ANIMATION_TYPE_SWIPE_CANCEL; + } + getSelectedDxDy(mTmpPosition); + final float currentTranslateX = mTmpPosition[0]; + final float currentTranslateY = mTmpPosition[1]; + final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, + prevActionState, currentTranslateX, currentTranslateY, + targetTranslateX, targetTranslateY) { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (this.mOverridden) { + return; + } + if (swipeDir <= 0) { + // this is a drag or failed swipe. recover immediately + mCallback.clearView(mRecyclerView, prevSelected); + // full cleanup will happen on onDrawOver + } else { + // wait until remove animation is complete. + mPendingCleanup.add(prevSelected.itemView); + mIsPendingCleanup = true; + if (swipeDir > 0) { + // Animation might be ended by other animators during a layout. + // We defer callback to avoid editing adapter during a layout. + postDispatchSwipe(this, swipeDir); + } + } + // removed from the list after it is drawn for the last time + if (mOverdrawChild == prevSelected.itemView) { + removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); + } + } + }; + final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, + targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); + rv.setDuration(duration); + mRecoverAnimations.add(rv); + rv.start(); + preventLayout = true; + } else { + removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); + mCallback.clearView(mRecyclerView, prevSelected); + } + mSelected = null; + } + if (selected != null) { + mSelectedFlags = + (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) + >> (mActionState * DIRECTION_FLAG_COUNT); + mSelectedStartX = selected.itemView.getLeft(); + mSelectedStartY = selected.itemView.getTop(); + mSelected = selected; + + if (actionState == ACTION_STATE_DRAG) { + mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + final ViewParent rvParent = mRecyclerView.getParent(); + if (rvParent != null) { + rvParent.requestDisallowInterceptTouchEvent(mSelected != null); + } + if (!preventLayout) { + mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); + } + mCallback.onSelectedChanged(mSelected, mActionState); + mRecyclerView.invalidate(); + } + + void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { + // wait until animations are complete. + mRecyclerView.post(new Runnable() { + @Override + public void run() { + if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() + && !anim.mOverridden + && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) { + final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); + // if animator is running or we have other active recover animations, we try + // not to call onSwiped because DefaultItemAnimator is not good at merging + // animations. Instead, we wait and batch. + if ((animator == null || !animator.isRunning(null)) + && !hasRunningRecoverAnim()) { + mCallback.onSwiped(anim.mViewHolder, swipeDir); + } else { + mRecyclerView.post(this); + } + } + } + }); + } + + boolean hasRunningRecoverAnim() { + final int size = mRecoverAnimations.size(); + for (int i = 0; i < size; i++) { + if (!mRecoverAnimations.get(i).mEnded) { + return true; + } + } + return false; + } + + /** + * If user drags the view to the edge, trigger a scroll if necessary. + */ + boolean scrollIfNecessary() { + if (mSelected == null) { + mDragScrollStartTimeInMs = Long.MIN_VALUE; + return false; + } + final long now = System.currentTimeMillis(); + final long scrollDuration = mDragScrollStartTimeInMs + == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; + RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + if (mTmpRect == null) { + mTmpRect = new Rect(); + } + int scrollX = 0; + int scrollY = 0; + lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); + if (lm.canScrollHorizontally()) { + int curX = (int) (mSelectedStartX + mDx); + final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); + if (mDx < 0 && leftDiff < 0) { + scrollX = leftDiff; + } else if (mDx > 0) { + final int rightDiff = + curX + mSelected.itemView.getWidth() + mTmpRect.right + - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); + if (rightDiff > 0) { + scrollX = rightDiff; + } + } + } + if (lm.canScrollVertically()) { + int curY = (int) (mSelectedStartY + mDy); + final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); + if (mDy < 0 && topDiff < 0) { + scrollY = topDiff; + } else if (mDy > 0) { + final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom + - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); + if (bottomDiff > 0) { + scrollY = bottomDiff; + } + } + } + if (scrollX != 0) { + scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, + mSelected.itemView.getWidth(), scrollX, + mRecyclerView.getWidth(), scrollDuration); + } + if (scrollY != 0) { + scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, + mSelected.itemView.getHeight(), scrollY, + mRecyclerView.getHeight(), scrollDuration); + } + if (scrollX != 0 || scrollY != 0) { + if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { + mDragScrollStartTimeInMs = now; + } + mRecyclerView.scrollBy(scrollX, scrollY); + return true; + } + mDragScrollStartTimeInMs = Long.MIN_VALUE; + return false; + } + + private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) { + if (mSwapTargets == null) { + mSwapTargets = new ArrayList<ViewHolder>(); + mDistances = new ArrayList<Integer>(); + } else { + mSwapTargets.clear(); + mDistances.clear(); + } + final int margin = mCallback.getBoundingBoxMargin(); + final int left = Math.round(mSelectedStartX + mDx) - margin; + final int top = Math.round(mSelectedStartY + mDy) - margin; + final int right = left + viewHolder.itemView.getWidth() + 2 * margin; + final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; + final int centerX = (left + right) / 2; + final int centerY = (top + bottom) / 2; + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + final int childCount = lm.getChildCount(); + for (int i = 0; i < childCount; i++) { + View other = lm.getChildAt(i); + if (other == viewHolder.itemView) { + continue; //myself! + } + if (other.getBottom() < top || other.getTop() > bottom + || other.getRight() < left || other.getLeft() > right) { + continue; + } + final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); + if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { + // find the index to add + final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); + final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); + final int dist = dx * dx + dy * dy; + + int pos = 0; + final int cnt = mSwapTargets.size(); + for (int j = 0; j < cnt; j++) { + if (dist > mDistances.get(j)) { + pos++; + } else { + break; + } + } + mSwapTargets.add(pos, otherVh); + mDistances.add(pos, dist); + } + } + return mSwapTargets; + } + + /** + * Checks if we should swap w/ another view holder. + */ + void moveIfNecessary(ViewHolder viewHolder) { + if (mRecyclerView.isLayoutRequested()) { + return; + } + if (mActionState != ACTION_STATE_DRAG) { + return; + } + + final float threshold = mCallback.getMoveThreshold(viewHolder); + final int x = (int) (mSelectedStartX + mDx); + final int y = (int) (mSelectedStartY + mDy); + if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold + && Math.abs(x - viewHolder.itemView.getLeft()) + < viewHolder.itemView.getWidth() * threshold) { + return; + } + List<ViewHolder> swapTargets = findSwapTargets(viewHolder); + if (swapTargets.size() == 0) { + return; + } + // may swap. + ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); + if (target == null) { + mSwapTargets.clear(); + mDistances.clear(); + return; + } + final int toPosition = target.getAdapterPosition(); + final int fromPosition = viewHolder.getAdapterPosition(); + if (mCallback.onMove(mRecyclerView, viewHolder, target)) { + // keep target visible + mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, + target, toPosition, x, y); + } + } + + @Override + public void onChildViewAttachedToWindow(View view) { + } + + @Override + public void onChildViewDetachedFromWindow(View view) { + removeChildDrawingOrderCallbackIfNecessary(view); + final ViewHolder holder = mRecyclerView.getChildViewHolder(view); + if (holder == null) { + return; + } + if (mSelected != null && holder == mSelected) { + select(null, ACTION_STATE_IDLE); + } else { + endRecoverAnimation(holder, false); // this may push it into pending cleanup list. + if (mPendingCleanup.remove(holder.itemView)) { + mCallback.clearView(mRecyclerView, holder); + } + } + } + + /** + * Returns the animation type or 0 if cannot be found. + */ + int endRecoverAnimation(ViewHolder viewHolder, boolean override) { + final int recoverAnimSize = mRecoverAnimations.size(); + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + if (anim.mViewHolder == viewHolder) { + anim.mOverridden |= override; + if (!anim.mEnded) { + anim.cancel(); + } + mRecoverAnimations.remove(i); + return anim.mAnimationType; + } + } + return 0; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + outRect.setEmpty(); + } + + void obtainVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + } + mVelocityTracker = VelocityTracker.obtain(); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private ViewHolder findSwipedView(MotionEvent motionEvent) { + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { + return null; + } + final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); + final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; + final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); + + if (absDx < mSlop && absDy < mSlop) { + return null; + } + if (absDx > absDy && lm.canScrollHorizontally()) { + return null; + } else if (absDy > absDx && lm.canScrollVertically()) { + return null; + } + View child = findChildView(motionEvent); + if (child == null) { + return null; + } + return mRecyclerView.getChildViewHolder(child); + } + + /** + * Checks whether we should select a View for swiping. + */ + boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { + if (mSelected != null || action != MotionEvent.ACTION_MOVE + || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { + return false; + } + if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { + return false; + } + final ViewHolder vh = findSwipedView(motionEvent); + if (vh == null) { + return false; + } + final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); + + final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) + >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); + + if (swipeFlags == 0) { + return false; + } + + // mDx and mDy are only set in allowed directions. We use custom x/y here instead of + // updateDxDy to avoid swiping if user moves more in the other direction + final float x = motionEvent.getX(pointerIndex); + final float y = motionEvent.getY(pointerIndex); + + // Calculate the distance moved + final float dx = x - mInitialTouchX; + final float dy = y - mInitialTouchY; + // swipe target is chose w/o applying flags so it does not really check if swiping in that + // direction is allowed. This why here, we use mDx mDy to check slope value again. + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); + + if (absDx < mSlop && absDy < mSlop) { + return false; + } + if (absDx > absDy) { + if (dx < 0 && (swipeFlags & LEFT) == 0) { + return false; + } + if (dx > 0 && (swipeFlags & RIGHT) == 0) { + return false; + } + } else { + if (dy < 0 && (swipeFlags & UP) == 0) { + return false; + } + if (dy > 0 && (swipeFlags & DOWN) == 0) { + return false; + } + } + mDx = mDy = 0f; + mActivePointerId = motionEvent.getPointerId(0); + select(vh, ACTION_STATE_SWIPE); + return true; + } + + View findChildView(MotionEvent event) { + // first check elevated views, if none, then call RV + final float x = event.getX(); + final float y = event.getY(); + if (mSelected != null) { + final View selectedView = mSelected.itemView; + if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { + return selectedView; + } + } + for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + final View view = anim.mViewHolder.itemView; + if (hitTest(view, x, y, anim.mX, anim.mY)) { + return view; + } + } + return mRecyclerView.findChildViewUnder(x, y); + } + + /** + * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a + * View is long pressed. You can disable that behavior by overriding + * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}. + * <p> + * For this method to work: + * <ul> + * <li>The provided ViewHolder must be a child of the RecyclerView to which this + * ItemTouchHelper + * is attached.</li> + * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li> + * <li>There must be a previous touch event that was reported to the ItemTouchHelper + * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener + * grabs previous events, this should work as expected.</li> + * </ul> + * + * For example, if you would like to let your user to be able to drag an Item by touching one + * of its descendants, you may implement it as follows: + * <pre> + * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { + * public boolean onTouch(View v, MotionEvent event) { + * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { + * mItemTouchHelper.startDrag(viewHolder); + * } + * return false; + * } + * }); + * </pre> + * <p> + * + * @param viewHolder The ViewHolder to start dragging. It must be a direct child of + * RecyclerView. + * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled() + */ + public void startDrag(ViewHolder viewHolder) { + if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { + Log.e(TAG, "Start drag has been called but dragging is not enabled"); + return; + } + if (viewHolder.itemView.getParent() != mRecyclerView) { + Log.e(TAG, "Start drag has been called with a view holder which is not a child of " + + "the RecyclerView which is controlled by this ItemTouchHelper."); + return; + } + obtainVelocityTracker(); + mDx = mDy = 0f; + select(viewHolder, ACTION_STATE_DRAG); + } + + /** + * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View + * when user swipes their finger (or mouse pointer) over the View. You can disable this + * behavior + * by overriding {@link ItemTouchHelper.Callback} + * <p> + * For this method to work: + * <ul> + * <li>The provided ViewHolder must be a child of the RecyclerView to which this + * ItemTouchHelper is attached.</li> + * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li> + * <li>There must be a previous touch event that was reported to the ItemTouchHelper + * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener + * grabs previous events, this should work as expected.</li> + * </ul> + * + * For example, if you would like to let your user to be able to swipe an Item by touching one + * of its descendants, you may implement it as follows: + * <pre> + * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() { + * public boolean onTouch(View v, MotionEvent event) { + * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) { + * mItemTouchHelper.startSwipe(viewHolder); + * } + * return false; + * } + * }); + * </pre> + * + * @param viewHolder The ViewHolder to start swiping. It must be a direct child of + * RecyclerView. + */ + public void startSwipe(ViewHolder viewHolder) { + if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { + Log.e(TAG, "Start swipe has been called but swiping is not enabled"); + return; + } + if (viewHolder.itemView.getParent() != mRecyclerView) { + Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " + + "the RecyclerView controlled by this ItemTouchHelper."); + return; + } + obtainVelocityTracker(); + mDx = mDy = 0f; + select(viewHolder, ACTION_STATE_SWIPE); + } + + RecoverAnimation findAnimation(MotionEvent event) { + if (mRecoverAnimations.isEmpty()) { + return null; + } + View target = findChildView(event); + for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + if (anim.mViewHolder.itemView == target) { + return anim; + } + } + return null; + } + + void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + + // Calculate the distance moved + mDx = x - mInitialTouchX; + mDy = y - mInitialTouchY; + if ((directionFlags & LEFT) == 0) { + mDx = Math.max(0, mDx); + } + if ((directionFlags & RIGHT) == 0) { + mDx = Math.min(0, mDx); + } + if ((directionFlags & UP) == 0) { + mDy = Math.max(0, mDy); + } + if ((directionFlags & DOWN) == 0) { + mDy = Math.min(0, mDy); + } + } + + private int swipeIfNecessary(ViewHolder viewHolder) { + if (mActionState == ACTION_STATE_DRAG) { + return 0; + } + final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); + final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( + originalMovementFlags, + mRecyclerView.getLayoutDirection()); + final int flags = (absoluteMovementFlags + & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); + if (flags == 0) { + return 0; + } + final int originalFlags = (originalMovementFlags + & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); + int swipeDir; + if (Math.abs(mDx) > Math.abs(mDy)) { + if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { + // if swipe dir is not in original flags, it should be the relative direction + if ((originalFlags & swipeDir) == 0) { + // convert to relative + return Callback.convertToRelativeDirection(swipeDir, + mRecyclerView.getLayoutDirection()); + } + return swipeDir; + } + if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { + return swipeDir; + } + } else { + if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { + return swipeDir; + } + if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { + // if swipe dir is not in original flags, it should be the relative direction + if ((originalFlags & swipeDir) == 0) { + // convert to relative + return Callback.convertToRelativeDirection(swipeDir, + mRecyclerView.getLayoutDirection()); + } + return swipeDir; + } + } + return 0; + } + + private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { + if ((flags & (LEFT | RIGHT)) != 0) { + final int dirFlag = mDx > 0 ? RIGHT : LEFT; + if (mVelocityTracker != null && mActivePointerId > -1) { + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, + mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); + final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); + final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); + final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; + final float absXVelocity = Math.abs(xVelocity); + if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag + && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) + && absXVelocity > Math.abs(yVelocity)) { + return velDirFlag; + } + } + + final float threshold = mRecyclerView.getWidth() * mCallback + .getSwipeThreshold(viewHolder); + + if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { + return dirFlag; + } + } + return 0; + } + + private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { + if ((flags & (UP | DOWN)) != 0) { + final int dirFlag = mDy > 0 ? DOWN : UP; + if (mVelocityTracker != null && mActivePointerId > -1) { + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, + mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); + final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); + final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); + final int velDirFlag = yVelocity > 0f ? DOWN : UP; + final float absYVelocity = Math.abs(yVelocity); + if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag + && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) + && absYVelocity > Math.abs(xVelocity)) { + return velDirFlag; + } + } + + final float threshold = mRecyclerView.getHeight() * mCallback + .getSwipeThreshold(viewHolder); + if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { + return dirFlag; + } + } + return 0; + } + + private void addChildDrawingOrderCallback() { + if (Build.VERSION.SDK_INT >= 21) { + return; // we use elevation on Lollipop + } + if (mChildDrawingOrderCallback == null) { + mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { + @Override + public int onGetChildDrawingOrder(int childCount, int i) { + if (mOverdrawChild == null) { + return i; + } + int childPosition = mOverdrawChildPosition; + if (childPosition == -1) { + childPosition = mRecyclerView.indexOfChild(mOverdrawChild); + mOverdrawChildPosition = childPosition; + } + if (i == childCount - 1) { + return childPosition; + } + return i < childPosition ? i : i + 1; + } + }; + } + mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); + } + + void removeChildDrawingOrderCallbackIfNecessary(View view) { + if (view == mOverdrawChild) { + mOverdrawChild = null; + // only remove if we've added + if (mChildDrawingOrderCallback != null) { + mRecyclerView.setChildDrawingOrderCallback(null); + } + } + } + + /** + * An interface which can be implemented by LayoutManager for better integration with + * {@link ItemTouchHelper}. + */ + public interface ViewDropHandler { + + /** + * Called by the {@link ItemTouchHelper} after a View is dropped over another View. + * <p> + * A LayoutManager should implement this interface to get ready for the upcoming move + * operation. + * <p> + * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that + * the View under drag will be used as an anchor View while calculating the next layout, + * making layout stay consistent. + * + * @param view The View which is being dragged. It is very likely that user is still + * dragging this View so there might be other + * {@link #prepareForDrop(View, View, int, int)} after this one. + * @param target The target view which is being dropped on. + * @param x The <code>left</code> offset of the View that is being dragged. This value + * includes the movement caused by the user. + * @param y The <code>top</code> offset of the View that is being dragged. This value + * includes the movement caused by the user. + */ + void prepareForDrop(View view, View target, int x, int y); + } + + /** + * This class is the contract between ItemTouchHelper and your application. It lets you control + * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user + * performs these actions. + * <p> + * To control which actions user can take on each view, you should override + * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set + * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, + * {@link #UP}, {@link #DOWN}). You can use + * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use + * {@link SimpleCallback}. + * <p> + * If user drags an item, ItemTouchHelper will call + * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) + * onMove(recyclerView, dragged, target)}. + * Upon receiving this callback, you should move the item from the old position + * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) + * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. + * To control where a View can be dropped, you can override + * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a + * dragging View overlaps multiple other views, Callback chooses the closest View with which + * dragged View might have changed positions. Although this approach works for many use cases, + * if you have a custom LayoutManager, you can override + * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a + * custom drop target. + * <p> + * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls + * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your + * adapter (e.g. remove the item) and call related Adapter#notify event. + */ + @SuppressWarnings("UnusedParameters") + public abstract static class Callback { + + public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; + + public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; + + static final int RELATIVE_DIR_FLAGS = START | END + | ((START | END) << DIRECTION_FLAG_COUNT) + | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); + + private static final ItemTouchUIUtil sUICallback = new ItemTouchUIUtilImpl(); + + private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT + | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) + | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); + + private static final Interpolator sDragScrollInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + return t * t * t * t * t; + } + }; + + private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + /** + * Drag scroll speed keeps accelerating until this many milliseconds before being capped. + */ + private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; + + private int mCachedMaxScrollSpeed = -1; + + /** + * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for + * visual + * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different + * implementations for different platform versions. + * <p> + * By default, {@link Callback} applies these changes on + * {@link RecyclerView.ViewHolder#itemView}. + * <p> + * For example, if you have a use case where you only want the text to move when user + * swipes over the view, you can do the following: + * <pre> + * public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){ + * getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView); + * } + * public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + * if (viewHolder != null){ + * getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView); + * } + * } + * public void onChildDraw(Canvas c, RecyclerView recyclerView, + * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, + * boolean isCurrentlyActive) { + * getDefaultUIUtil().onDraw(c, recyclerView, + * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, + * actionState, isCurrentlyActive); + * return true; + * } + * public void onChildDrawOver(Canvas c, RecyclerView recyclerView, + * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, + * boolean isCurrentlyActive) { + * getDefaultUIUtil().onDrawOver(c, recyclerView, + * ((ItemTouchViewHolder) viewHolder).textView, dX, dY, + * actionState, isCurrentlyActive); + * return true; + * } + * </pre> + * + * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback} + */ + public static ItemTouchUIUtil getDefaultUIUtil() { + return sUICallback; + } + + /** + * Replaces a movement direction with its relative version by taking layout direction into + * account. + * + * @param flags The flag value that include any number of movement flags. + * @param layoutDirection The layout direction of the View. Can be obtained from + * {@link View#getLayoutDirection()}. + * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead + * of {@link #LEFT}, {@link #RIGHT}. + * @see #convertToAbsoluteDirection(int, int) + */ + public static int convertToRelativeDirection(int flags, int layoutDirection) { + int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; + if (masked == 0) { + return flags; // does not have any abs flags, good. + } + flags &= ~masked; //remove left / right. + if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { + // no change. just OR with 2 bits shifted mask and return + flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. + return flags; + } else { + // add RIGHT flag as START + flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); + // first clean RIGHT bit then add LEFT flag as END + flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; + } + return flags; + } + + /** + * Convenience method to create movement flags. + * <p> + * For instance, if you want to let your items be drag & dropped vertically and swiped + * left to be dismissed, you can call this method with: + * <code>makeMovementFlags(UP | DOWN, LEFT);</code> + * + * @param dragFlags The directions in which the item can be dragged. + * @param swipeFlags The directions in which the item can be swiped. + * @return Returns an integer composed of the given drag and swipe flags. + */ + public static int makeMovementFlags(int dragFlags, int swipeFlags) { + return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) + | makeFlag(ACTION_STATE_SWIPE, swipeFlags) + | makeFlag(ACTION_STATE_DRAG, dragFlags); + } + + /** + * Shifts the given direction flags to the offset of the given action state. + * + * @param actionState The action state you want to get flags in. Should be one of + * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or + * {@link #ACTION_STATE_DRAG}. + * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, + * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. + * @return And integer that represents the given directions in the provided actionState. + */ + public static int makeFlag(int actionState, int directions) { + return directions << (actionState * DIRECTION_FLAG_COUNT); + } + + /** + * Should return a composite flag which defines the enabled move directions in each state + * (idle, swiping, dragging). + * <p> + * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, + * int)} + * or {@link #makeFlag(int, int)}. + * <p> + * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next + * 8 bits are for SWIPE state and third 8 bits are for DRAG state. + * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in + * {@link ItemTouchHelper}. + * <p> + * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to + * swipe by swiping RIGHT, you can return: + * <pre> + * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); + * </pre> + * This means, allow right movement while IDLE and allow right and left movement while + * swiping. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. + * @param viewHolder The ViewHolder for which the movement information is necessary. + * @return flags specifying which movements are allowed on this ViewHolder. + * @see #makeMovementFlags(int, int) + * @see #makeFlag(int, int) + */ + public abstract int getMovementFlags(RecyclerView recyclerView, + ViewHolder viewHolder); + + /** + * Converts a given set of flags to absolution direction which means {@link #START} and + * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout + * direction. + * + * @param flags The flag value that include any number of movement flags. + * @param layoutDirection The layout direction of the RecyclerView. + * @return Updated flags which includes only absolute direction values. + */ + public int convertToAbsoluteDirection(int flags, int layoutDirection) { + int masked = flags & RELATIVE_DIR_FLAGS; + if (masked == 0) { + return flags; // does not have any relative flags, good. + } + flags &= ~masked; //remove start / end + if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { + // no change. just OR with 2 bits shifted mask and return + flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. + return flags; + } else { + // add START flag as RIGHT + flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); + // first clean start bit then add END flag as LEFT + flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; + } + return flags; + } + + final int getAbsoluteMovementFlags(RecyclerView recyclerView, + ViewHolder viewHolder) { + final int flags = getMovementFlags(recyclerView, viewHolder); + return convertToAbsoluteDirection(flags, recyclerView.getLayoutDirection()); + } + + boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { + final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); + return (flags & ACTION_MODE_DRAG_MASK) != 0; + } + + boolean hasSwipeFlag(RecyclerView recyclerView, + ViewHolder viewHolder) { + final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); + return (flags & ACTION_MODE_SWIPE_MASK) != 0; + } + + /** + * Return true if the current ViewHolder can be dropped over the the target ViewHolder. + * <p> + * This method is used when selecting drop target for the dragged View. After Views are + * eliminated either via bounds check or via this method, resulting set of views will be + * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}. + * <p> + * Default implementation returns true. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. + * @param current The ViewHolder that user is dragging. + * @param target The ViewHolder which is below the dragged ViewHolder. + * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false + * otherwise. + */ + public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, + ViewHolder target) { + return true; + } + + /** + * Called when ItemTouchHelper wants to move the dragged item from its old position to + * the new position. + * <p> + * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved + * to the adapter position of {@code target} ViewHolder + * ({@link ViewHolder#getAdapterPosition() + * ViewHolder#getAdapterPosition()}). + * <p> + * If you don't support drag & drop, this method will never be called. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. + * @param viewHolder The ViewHolder which is being dragged by the user. + * @param target The ViewHolder over which the currently active item is being + * dragged. + * @return True if the {@code viewHolder} has been moved to the adapter position of + * {@code target}. + * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) + */ + public abstract boolean onMove(RecyclerView recyclerView, + ViewHolder viewHolder, ViewHolder target); + + /** + * Returns whether ItemTouchHelper should start a drag and drop operation if an item is + * long pressed. + * <p> + * Default value returns true but you may want to disable this if you want to start + * dragging on a custom view touch using {@link #startDrag(ViewHolder)}. + * + * @return True if ItemTouchHelper should start dragging an item when it is long pressed, + * false otherwise. Default value is <code>true</code>. + * @see #startDrag(ViewHolder) + */ + public boolean isLongPressDragEnabled() { + return true; + } + + /** + * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped + * over the View. + * <p> + * Default value returns true but you may want to disable this if you want to start + * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. + * + * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer + * over the View, false otherwise. Default value is <code>true</code>. + * @see #startSwipe(ViewHolder) + */ + public boolean isItemViewSwipeEnabled() { + return true; + } + + /** + * When finding views under a dragged view, by default, ItemTouchHelper searches for views + * that overlap with the dragged View. By overriding this method, you can extend or shrink + * the search box. + * + * @return The extra margin to be added to the hit box of the dragged View. + */ + public int getBoundingBoxMargin() { + return 0; + } + + /** + * Returns the fraction that the user should move the View to be considered as swiped. + * The fraction is calculated with respect to RecyclerView's bounds. + * <p> + * Default value is .5f, which means, to swipe a View, user must move the View at least + * half of RecyclerView's width or height, depending on the swipe direction. + * + * @param viewHolder The ViewHolder that is being dragged. + * @return A float value that denotes the fraction of the View size. Default value + * is .5f . + */ + public float getSwipeThreshold(ViewHolder viewHolder) { + return .5f; + } + + /** + * Returns the fraction that the user should move the View to be considered as it is + * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views + * below it for a possible drop. + * + * @param viewHolder The ViewHolder that is being dragged. + * @return A float value that denotes the fraction of the View size. Default value is + * .5f . + */ + public float getMoveThreshold(ViewHolder viewHolder) { + return .5f; + } + + /** + * Defines the minimum velocity which will be considered as a swipe action by the user. + * <p> + * You can increase this value to make it harder to swipe or decrease it to make it easier. + * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure + * current direction velocity is larger then the perpendicular one. Otherwise, user's + * movement is ambiguous. You can change the threshold by overriding + * {@link #getSwipeVelocityThreshold(float)}. + * <p> + * The velocity is calculated in pixels per second. + * <p> + * The default framework value is passed as a parameter so that you can modify it with a + * multiplier. + * + * @param defaultValue The default value (in pixels per second) used by the + * ItemTouchHelper. + * @return The minimum swipe velocity. The default implementation returns the + * <code>defaultValue</code> parameter. + * @see #getSwipeVelocityThreshold(float) + * @see #getSwipeThreshold(ViewHolder) + */ + public float getSwipeEscapeVelocity(float defaultValue) { + return defaultValue; + } + + /** + * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. + * <p> + * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the + * perpendicular movement. If both directions reach to the max threshold, none of them will + * be considered as a swipe because it is usually an indication that user rather tried to + * scroll then swipe. + * <p> + * The velocity is calculated in pixels per second. + * <p> + * You can customize this behavior by changing this method. If you increase the value, it + * will be easier for the user to swipe diagonally and if you decrease the value, user will + * need to make a rather straight finger movement to trigger a swipe. + * + * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper. + * @return The velocity cap for pointer movements. The default implementation returns the + * <code>defaultValue</code> parameter. + * @see #getSwipeEscapeVelocity(float) + */ + public float getSwipeVelocityThreshold(float defaultValue) { + return defaultValue; + } + + /** + * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that + * are under the dragged View. + * <p> + * Default implementation filters the View with which dragged item have changed position + * in the drag direction. For instance, if the view is dragged UP, it compares the + * <code>view.getTop()</code> of the two views before and after drag started. If that value + * is different, the target view passes the filter. + * <p> + * Among these Views which pass the test, the one closest to the dragged view is chosen. + * <p> + * This method is called on the main thread every time user moves the View. If you want to + * override it, make sure it does not do any expensive operations. + * + * @param selected The ViewHolder being dragged by the user. + * @param dropTargets The list of ViewHolder that are under the dragged View and + * candidate as a drop. + * @param curX The updated left value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @param curY The updated top value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @return A ViewHolder to whose position the dragged ViewHolder should be + * moved to. + */ + public ViewHolder chooseDropTarget(ViewHolder selected, + List<ViewHolder> dropTargets, int curX, int curY) { + int right = curX + selected.itemView.getWidth(); + int bottom = curY + selected.itemView.getHeight(); + ViewHolder winner = null; + int winnerScore = -1; + final int dx = curX - selected.itemView.getLeft(); + final int dy = curY - selected.itemView.getTop(); + final int targetsSize = dropTargets.size(); + for (int i = 0; i < targetsSize; i++) { + final ViewHolder target = dropTargets.get(i); + if (dx > 0) { + int diff = target.itemView.getRight() - right; + if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + if (dx < 0) { + int diff = target.itemView.getLeft() - curX; + if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + if (dy < 0) { + int diff = target.itemView.getTop() - curY; + if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + + if (dy > 0) { + int diff = target.itemView.getBottom() - bottom; + if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + } + return winner; + } + + /** + * Called when a ViewHolder is swiped by the user. + * <p> + * If you are returning relative directions ({@link #START} , {@link #END}) from the + * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method + * will also use relative directions. Otherwise, it will use absolute directions. + * <p> + * If you don't support swiping, this method will never be called. + * <p> + * ItemTouchHelper will keep a reference to the View until it is detached from + * RecyclerView. + * As soon as it is detached, ItemTouchHelper will call + * {@link #clearView(RecyclerView, ViewHolder)}. + * + * @param viewHolder The ViewHolder which has been swiped by the user. + * @param direction The direction to which the ViewHolder is swiped. It is one of + * {@link #UP}, {@link #DOWN}, + * {@link #LEFT} or {@link #RIGHT}. If your + * {@link #getMovementFlags(RecyclerView, ViewHolder)} + * method + * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; + * `direction` will be relative as well. ({@link #START} or {@link + * #END}). + */ + public abstract void onSwiped(ViewHolder viewHolder, int direction); + + /** + * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. + * <p/> + * If you override this method, you should call super. + * + * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if + * it is cleared. + * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, + * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or + * {@link ItemTouchHelper#ACTION_STATE_DRAG}. + * @see #clearView(RecyclerView, RecyclerView.ViewHolder) + */ + public void onSelectedChanged(ViewHolder viewHolder, int actionState) { + if (viewHolder != null) { + sUICallback.onSelected(viewHolder.itemView); + } + } + + private int getMaxDragScroll(RecyclerView recyclerView) { + if (mCachedMaxScrollSpeed == -1) { + mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( + R.dimen.item_touch_helper_max_drag_scroll_per_frame); + } + return mCachedMaxScrollSpeed; + } + + /** + * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. + * <p> + * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it + * modifies the existing View. Because of this reason, it is important that the View is + * still part of the layout after it is moved. This may not work as intended when swapped + * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views + * which were not eligible for dropping over). + * <p> + * This method is responsible to give necessary hint to the LayoutManager so that it will + * keep the View in visible area. For example, for LinearLayoutManager, this is as simple + * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. + * + * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's + * new position is likely to be out of bounds. + * <p> + * It is important to ensure the ViewHolder will stay visible as otherwise, it might be + * removed by the LayoutManager if the move causes the View to go out of bounds. In that + * case, drag will end prematurely. + * + * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. + * @param viewHolder The ViewHolder under user's control. + * @param fromPos The previous adapter position of the dragged item (before it was + * moved). + * @param target The ViewHolder on which the currently active item has been dropped. + * @param toPos The new adapter position of the dragged item. + * @param x The updated left value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @param y The updated top value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + */ + public void onMoved(final RecyclerView recyclerView, + final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x, + int y) { + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof ViewDropHandler) { + ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, + target.itemView, x, y); + return; + } + + // if layout manager cannot handle it, do some guesswork + if (layoutManager.canScrollHorizontally()) { + final int minLeft = layoutManager.getDecoratedLeft(target.itemView); + if (minLeft <= recyclerView.getPaddingLeft()) { + recyclerView.scrollToPosition(toPos); + } + final int maxRight = layoutManager.getDecoratedRight(target.itemView); + if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { + recyclerView.scrollToPosition(toPos); + } + } + + if (layoutManager.canScrollVertically()) { + final int minTop = layoutManager.getDecoratedTop(target.itemView); + if (minTop <= recyclerView.getPaddingTop()) { + recyclerView.scrollToPosition(toPos); + } + final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); + if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { + recyclerView.scrollToPosition(toPos); + } + } + } + + void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, + List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); + for (int i = 0; i < recoverAnimSize; i++) { + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + anim.update(); + final int count = c.save(); + onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, + false); + c.restoreToCount(count); + } + if (selected != null) { + final int count = c.save(); + onChildDraw(c, parent, selected, dX, dY, actionState, true); + c.restoreToCount(count); + } + } + + void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, + List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); + for (int i = 0; i < recoverAnimSize; i++) { + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + final int count = c.save(); + onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, + false); + c.restoreToCount(count); + } + if (selected != null) { + final int count = c.save(); + onChildDrawOver(c, parent, selected, dX, dY, actionState, true); + c.restoreToCount(count); + } + boolean hasRunningAnimation = false; + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation anim = recoverAnimationList.get(i); + if (anim.mEnded && !anim.mIsPendingCleanup) { + recoverAnimationList.remove(i); + } else if (!anim.mEnded) { + hasRunningAnimation = true; + } + } + if (hasRunningAnimation) { + parent.invalidate(); + } + } + + /** + * Called by the ItemTouchHelper when the user interaction with an element is over and it + * also completed its animation. + * <p> + * This is a good place to clear all changes on the View that was done in + * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, + * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean)} or + * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. + * + * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. + * @param viewHolder The View that was interacted by the user. + */ + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + sUICallback.clearView(viewHolder.itemView); + } + + /** + * Called by ItemTouchHelper on RecyclerView's onDraw callback. + * <p> + * If you would like to customize how your View's respond to user interactions, this is + * a good place to override. + * <p> + * Default implementation translates the child by the given <code>dX</code>, + * <code>dY</code>. + * ItemTouchHelper also takes care of drawing the child after other children if it is being + * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this + * is + * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L + * and after, it changes View's elevation value to be greater than all other children.) + * + * @param c The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was + * interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either {@link + * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. + * @param isCurrentlyActive True if this view is currently being controlled by the user or + * false it is simply animating back to its original state. + * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean) + */ + public void onChildDraw(Canvas c, RecyclerView recyclerView, + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, + isCurrentlyActive); + } + + /** + * Called by ItemTouchHelper on RecyclerView's onDraw callback. + * <p> + * If you would like to customize how your View's respond to user interactions, this is + * a good place to override. + * <p> + * Default implementation translates the child by the given <code>dX</code>, + * <code>dY</code>. + * ItemTouchHelper also takes care of drawing the child after other children if it is being + * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this + * is + * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L + * and after, it changes View's elevation value to be greater than all other children.) + * + * @param c The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was + * interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either {@link + * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. + * @param isCurrentlyActive True if this view is currently being controlled by the user or + * false it is simply animating back to its original state. + * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean) + */ + public void onChildDrawOver(Canvas c, RecyclerView recyclerView, + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, + isCurrentlyActive); + } + + /** + * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View + * will be animated to its final position. + * <p> + * Default implementation uses ItemAnimator's duration values. If + * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns + * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns + * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have + * any {@link RecyclerView.ItemAnimator} attached, this method returns + * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} + * depending on the animation type. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, + * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or + * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. + * @param animateDx The horizontal distance that the animation will offset + * @param animateDy The vertical distance that the animation will offset + * @return The duration for the animation + */ + public long getAnimationDuration(RecyclerView recyclerView, int animationType, + float animateDx, float animateDy) { + final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); + if (itemAnimator == null) { + return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION + : DEFAULT_SWIPE_ANIMATION_DURATION; + } else { + return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() + : itemAnimator.getRemoveDuration(); + } + } + + /** + * Called by the ItemTouchHelper when user is dragging a view out of bounds. + * <p> + * You can override this method to decide how much RecyclerView should scroll in response + * to this action. Default implementation calculates a value based on the amount of View + * out of bounds and the time it spent there. The longer user keeps the View out of bounds, + * the faster the list will scroll. Similarly, the larger portion of the View is out of + * bounds, the faster the RecyclerView will scroll. + * + * @param recyclerView The RecyclerView instance to which ItemTouchHelper is + * attached to. + * @param viewSize The total size of the View in scroll direction, excluding + * item decorations. + * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value + * is negative if the View is dragged towards left or top edge. + * @param totalSize The total size of RecyclerView in the scroll direction. + * @param msSinceStartScroll The time passed since View is kept out of bounds. + * @return The amount that RecyclerView should scroll. Keep in mind that this value will + * be passed to {@link RecyclerView#scrollBy(int, int)} method. + */ + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, + int viewSize, int viewSizeOutOfBounds, + int totalSize, long msSinceStartScroll) { + final int maxScroll = getMaxDragScroll(recyclerView); + final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); + final int direction = (int) Math.signum(viewSizeOutOfBounds); + // might be negative if other direction + float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); + final int cappedScroll = (int) (direction * maxScroll + * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); + final float timeRatio; + if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { + timeRatio = 1f; + } else { + timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; + } + final int value = (int) (cappedScroll * sDragScrollInterpolator + .getInterpolation(timeRatio)); + if (value == 0) { + return viewSizeOutOfBounds > 0 ? 1 : -1; + } + return value; + } + } + + /** + * A simple wrapper to the default Callback which you can construct with drag and swipe + * directions and this class will handle the flag callbacks. You should still override onMove + * or + * onSwiped depending on your use case. + * + * <pre> + * ItemTouchHelper mIth = new ItemTouchHelper( + * new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + * ItemTouchHelper.LEFT) { + * public abstract boolean onMove(RecyclerView recyclerView, + * ViewHolder viewHolder, ViewHolder target) { + * final int fromPos = viewHolder.getAdapterPosition(); + * final int toPos = target.getAdapterPosition(); + * // move item in `fromPos` to `toPos` in adapter. + * return true;// true if moved, false otherwise + * } + * public void onSwiped(ViewHolder viewHolder, int direction) { + * // remove from adapter + * } + * }); + * </pre> + */ + public abstract static class SimpleCallback extends Callback { + + private int mDefaultSwipeDirs; + + private int mDefaultDragDirs; + + /** + * Creates a Callback for the given drag and swipe allowance. These values serve as + * defaults + * and if you want to customize behavior per ViewHolder, you can override + * {@link #getSwipeDirs(RecyclerView, ViewHolder)} + * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}. + * + * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be + * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link + * #END}, + * {@link #UP} and {@link #DOWN}. + * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be + * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link + * #END}, + * {@link #UP} and {@link #DOWN}. + */ + public SimpleCallback(int dragDirs, int swipeDirs) { + mDefaultSwipeDirs = swipeDirs; + mDefaultDragDirs = dragDirs; + } + + /** + * Updates the default swipe directions. For example, you can use this method to toggle + * certain directions depending on your use case. + * + * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. + */ + public void setDefaultSwipeDirs(int defaultSwipeDirs) { + mDefaultSwipeDirs = defaultSwipeDirs; + } + + /** + * Updates the default drag directions. For example, you can use this method to toggle + * certain directions depending on your use case. + * + * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. + */ + public void setDefaultDragDirs(int defaultDragDirs) { + mDefaultDragDirs = defaultDragDirs; + } + + /** + * Returns the swipe directions for the provided ViewHolder. + * Default implementation returns the swipe directions that was set via constructor or + * {@link #setDefaultSwipeDirs(int)}. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param viewHolder The RecyclerView for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + public int getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder) { + return mDefaultSwipeDirs; + } + + /** + * Returns the drag directions for the provided ViewHolder. + * Default implementation returns the drag directions that was set via constructor or + * {@link #setDefaultDragDirs(int)}. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param viewHolder The RecyclerView for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) { + return mDefaultDragDirs; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { + return makeMovementFlags(getDragDirs(recyclerView, viewHolder), + getSwipeDirs(recyclerView, viewHolder)); + } + } + + private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { + + ItemTouchHelperGestureListener() { + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + View child = findChildView(e); + if (child != null) { + ViewHolder vh = mRecyclerView.getChildViewHolder(child); + if (vh != null) { + if (!mCallback.hasDragFlag(mRecyclerView, vh)) { + return; + } + int pointerId = e.getPointerId(0); + // Long press is deferred. + // Check w/ active pointer id to avoid selecting after motion + // event is canceled. + if (pointerId == mActivePointerId) { + final int index = e.findPointerIndex(mActivePointerId); + final float x = e.getX(index); + final float y = e.getY(index); + mInitialTouchX = x; + mInitialTouchY = y; + mDx = mDy = 0f; + if (DEBUG) { + Log.d(TAG, + "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); + } + if (mCallback.isLongPressDragEnabled()) { + select(vh, ACTION_STATE_DRAG); + } + } + } + } + } + } + + private class RecoverAnimation implements Animator.AnimatorListener { + + final float mStartDx; + + final float mStartDy; + + final float mTargetX; + + final float mTargetY; + + final ViewHolder mViewHolder; + + final int mActionState; + + private final ValueAnimator mValueAnimator; + + final int mAnimationType; + + public boolean mIsPendingCleanup; + + float mX; + + float mY; + + // if user starts touching a recovering view, we put it into interaction mode again, + // instantly. + boolean mOverridden = false; + + boolean mEnded = false; + + private float mFraction; + + RecoverAnimation(ViewHolder viewHolder, int animationType, + int actionState, float startDx, float startDy, float targetX, float targetY) { + mActionState = actionState; + mAnimationType = animationType; + mViewHolder = viewHolder; + mStartDx = startDx; + mStartDy = startDy; + mTargetX = targetX; + mTargetY = targetY; + mValueAnimator = ValueAnimator.ofFloat(0f, 1f); + mValueAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setFraction(animation.getAnimatedFraction()); + } + }); + mValueAnimator.setTarget(viewHolder.itemView); + mValueAnimator.addListener(this); + setFraction(0f); + } + + public void setDuration(long duration) { + mValueAnimator.setDuration(duration); + } + + public void start() { + mViewHolder.setIsRecyclable(false); + mValueAnimator.start(); + } + + public void cancel() { + mValueAnimator.cancel(); + } + + public void setFraction(float fraction) { + mFraction = fraction; + } + + /** + * We run updates on onDraw method but use the fraction from animator callback. + * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. + */ + public void update() { + if (mStartDx == mTargetX) { + mX = mViewHolder.itemView.getTranslationX(); + } else { + mX = mStartDx + mFraction * (mTargetX - mStartDx); + } + if (mStartDy == mTargetY) { + mY = mViewHolder.itemView.getTranslationY(); + } else { + mY = mStartDy + mFraction * (mTargetY - mStartDy); + } + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mEnded) { + mViewHolder.setIsRecyclable(true); + } + mEnded = true; + } + + @Override + public void onAnimationCancel(Animator animation) { + setFraction(1f); //make sure we recover the view's state. + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + } +} diff --git a/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java b/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java new file mode 100644 index 000000000000..e368a6d41f09 --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget.helper; + +import android.graphics.Canvas; +import android.view.View; + +import com.android.internal.widget.RecyclerView; + +/** + * Utility class for {@link ItemTouchHelper} which handles item transformations for different + * API versions. + * <p/> + * This class has methods that map to {@link ItemTouchHelper.Callback}'s drawing methods. Default + * implementations in {@link ItemTouchHelper.Callback} call these methods with + * {@link RecyclerView.ViewHolder#itemView} and {@link ItemTouchUIUtil} makes necessary changes + * on the View depending on the API level. You can access the instance of {@link ItemTouchUIUtil} + * via {@link ItemTouchHelper.Callback#getDefaultUIUtil()} and call its methods with the children + * of ViewHolder that you want to apply default effects. + * + * @see ItemTouchHelper.Callback#getDefaultUIUtil() + */ +public interface ItemTouchUIUtil { + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas, + * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} + */ + void onDraw(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas, + * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} + */ + void onDrawOver(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView, + * RecyclerView.ViewHolder)} + */ + void clearView(View view); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged( + * RecyclerView.ViewHolder, int)} + */ + void onSelected(View view); +} + diff --git a/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java b/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java new file mode 100644 index 000000000000..0de240b8c683 --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget.helper; + +import android.graphics.Canvas; +import android.view.View; + +import com.android.internal.R; +import com.android.internal.widget.RecyclerView; + +/** + * Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them + * public API, which is not desired in this case. + */ +class ItemTouchUIUtilImpl implements ItemTouchUIUtil { + @Override + public void onDraw(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (isCurrentlyActive) { + Object originalElevation = view.getTag( + R.id.item_touch_helper_previous_elevation); + if (originalElevation == null) { + originalElevation = view.getElevation(); + float newElevation = 1f + findMaxElevation(recyclerView, view); + view.setElevation(newElevation); + view.setTag(R.id.item_touch_helper_previous_elevation, + originalElevation); + } + } + view.setTranslationX(dX); + view.setTranslationY(dY); + } + + private float findMaxElevation(RecyclerView recyclerView, View itemView) { + final int childCount = recyclerView.getChildCount(); + float max = 0; + for (int i = 0; i < childCount; i++) { + final View child = recyclerView.getChildAt(i); + if (child == itemView) { + continue; + } + final float elevation = child.getElevation(); + if (elevation > max) { + max = elevation; + } + } + return max; + } + + @Override + public void clearView(View view) { + final Object tag = view.getTag( + R.id.item_touch_helper_previous_elevation); + if (tag != null && tag instanceof Float) { + view.setElevation((Float) tag); + } + view.setTag(R.id.item_touch_helper_previous_elevation, null); + view.setTranslationX(0f); + view.setTranslationY(0f); + } + + @Override + public void onSelected(View view) { + } + + @Override + public void onDrawOver(Canvas c, RecyclerView recyclerView, + View view, float dX, float dY, int actionState, boolean isCurrentlyActive) { + } +} diff --git a/core/jni/Android.mk b/core/jni/Android.mk index 0d3ccdc87aa2..327f142fc319 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -158,6 +158,7 @@ LOCAL_SRC_FILES:= \ android_hardware_camera2_legacy_LegacyCameraDevice.cpp \ android_hardware_camera2_legacy_PerfMeasurement.cpp \ android_hardware_camera2_DngCreator.cpp \ + android_hardware_HardwareBuffer.cpp \ android_hardware_Radio.cpp \ android_hardware_SensorManager.cpp \ android_hardware_SerialPort.cpp \ diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index fb5d0373725a..340f2eefa37a 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -87,6 +87,7 @@ extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env); extern int register_android_hardware_camera2_legacy_LegacyCameraDevice(JNIEnv *env); extern int register_android_hardware_camera2_legacy_PerfMeasurement(JNIEnv *env); extern int register_android_hardware_camera2_DngCreator(JNIEnv *env); +extern int register_android_hardware_HardwareBuffer(JNIEnv *env); extern int register_android_hardware_Radio(JNIEnv *env); extern int register_android_hardware_SensorManager(JNIEnv *env); extern int register_android_hardware_SerialPort(JNIEnv *env); @@ -1373,6 +1374,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_hardware_camera2_legacy_LegacyCameraDevice), REG_JNI(register_android_hardware_camera2_legacy_PerfMeasurement), REG_JNI(register_android_hardware_camera2_DngCreator), + REG_JNI(register_android_hardware_HardwareBuffer), REG_JNI(register_android_hardware_Radio), REG_JNI(register_android_hardware_SensorManager), REG_JNI(register_android_hardware_SerialPort), diff --git a/core/jni/android/graphics/FontFamily.cpp b/core/jni/android/graphics/FontFamily.cpp index 2504e540ff14..ac8f413c2677 100644 --- a/core/jni/android/graphics/FontFamily.cpp +++ b/core/jni/android/graphics/FontFamily.cpp @@ -258,9 +258,9 @@ int register_android_graphics_FontFamily(JNIEnv* env) gListClassInfo.mGet = GetMethodIDOrDie(env, listClass, "get", "(I)Ljava/lang/Object;"); gListClassInfo.mSize = GetMethodIDOrDie(env, listClass, "size", "()I"); - jclass axisClass = FindClassOrDie(env, "android/graphics/FontListParser$Axis"); - gAxisClassInfo.mTag = GetFieldIDOrDie(env, axisClass, "tag", "I"); - gAxisClassInfo.mStyleValue = GetFieldIDOrDie(env, axisClass, "styleValue", "F"); + jclass axisClass = FindClassOrDie(env, "android/text/FontConfig$Axis"); + gAxisClassInfo.mTag = GetFieldIDOrDie(env, axisClass, "mTag", "I"); + gAxisClassInfo.mStyleValue = GetFieldIDOrDie(env, axisClass, "mStyleValue", "F"); return err; } diff --git a/core/jni/android/graphics/FontUtils.cpp b/core/jni/android/graphics/FontUtils.cpp new file mode 100644 index 000000000000..91fec2a75e2c --- /dev/null +++ b/core/jni/android/graphics/FontUtils.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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 "FontUtils.h" + +#include "JNIHelp.h" +#include <core_jni_helpers.h> + +namespace android { +namespace { + +static struct { + jmethodID mGet; + jmethodID mSize; +} gListClassInfo; + +static struct { + jfieldID mTag; + jfieldID mStyleValue; +} gAxisClassInfo; + +} // namespace + +jint ListHelper::size() const { + return mEnv->CallIntMethod(mList, gListClassInfo.mSize); +} + +jobject ListHelper::get(jint index) const { + return mEnv->CallObjectMethod(mList, gListClassInfo.mGet, index); +} + +jint AxisHelper::getTag() const { + return mEnv->GetIntField(mAxis, gAxisClassInfo.mTag); +} + +jfloat AxisHelper::getStyleValue() const { + return mEnv->GetFloatField(mAxis, gAxisClassInfo.mStyleValue); +} + +void init_FontUtils(JNIEnv* env) { + jclass listClass = FindClassOrDie(env, "java/util/List"); + gListClassInfo.mGet = GetMethodIDOrDie(env, listClass, "get", "(I)Ljava/lang/Object;"); + gListClassInfo.mSize = GetMethodIDOrDie(env, listClass, "size", "()I"); + + jclass axisClass = FindClassOrDie(env, "android/text/FontConfig$Axis"); + gAxisClassInfo.mTag = GetFieldIDOrDie(env, axisClass, "mTag", "I"); + gAxisClassInfo.mStyleValue = GetFieldIDOrDie(env, axisClass, "mStyleValue", "F"); +} + +} // namespace android diff --git a/core/jni/android_graphics_Canvas.cpp b/core/jni/android_graphics_Canvas.cpp index 9ce5670f904c..c49287ccddb2 100644 --- a/core/jni/android_graphics_Canvas.cpp +++ b/core/jni/android_graphics_Canvas.cpp @@ -63,97 +63,87 @@ static void setBitmap(JNIEnv* env, jobject, jlong canvasHandle, jobject jbitmap) get_canvas(canvasHandle)->setBitmap(bitmap); } -static jboolean isOpaque(JNIEnv*, jobject, jlong canvasHandle) { +static jboolean isOpaque(jlong canvasHandle) { return get_canvas(canvasHandle)->isOpaque() ? JNI_TRUE : JNI_FALSE; } -static jint getWidth(JNIEnv*, jobject, jlong canvasHandle) { +static jint getWidth(jlong canvasHandle) { return static_cast<jint>(get_canvas(canvasHandle)->width()); } -static jint getHeight(JNIEnv*, jobject, jlong canvasHandle) { +static jint getHeight(jlong canvasHandle) { return static_cast<jint>(get_canvas(canvasHandle)->height()); } -static void setHighContrastText(JNIEnv*, jobject, jlong canvasHandle, jboolean highContrastText) { +static void setHighContrastText(jlong canvasHandle, jboolean highContrastText) { Canvas* canvas = get_canvas(canvasHandle); canvas->setHighContrastText(highContrastText); } -static jint getSaveCount(JNIEnv*, jobject, jlong canvasHandle) { - return static_cast<jint>(get_canvas(canvasHandle)->getSaveCount()); -} - -static jint save(JNIEnv*, jobject, jlong canvasHandle, jint flagsHandle) { +static jint save(jlong canvasHandle, jint flagsHandle) { SaveFlags::Flags flags = static_cast<SaveFlags::Flags>(flagsHandle); return static_cast<jint>(get_canvas(canvasHandle)->save(flags)); } -static jint saveLayer(JNIEnv* env, jobject, jlong canvasHandle, jfloat l, jfloat t, +static jint saveLayer(jlong canvasHandle, jfloat l, jfloat t, jfloat r, jfloat b, jlong paintHandle, jint flagsHandle) { Paint* paint = reinterpret_cast<Paint*>(paintHandle); SaveFlags::Flags flags = static_cast<SaveFlags::Flags>(flagsHandle); return static_cast<jint>(get_canvas(canvasHandle)->saveLayer(l, t, r, b, paint, flags)); } -static jint saveLayerAlpha(JNIEnv* env, jobject, jlong canvasHandle, jfloat l, jfloat t, +static jint saveLayerAlpha(jlong canvasHandle, jfloat l, jfloat t, jfloat r, jfloat b, jint alpha, jint flagsHandle) { SaveFlags::Flags flags = static_cast<SaveFlags::Flags>(flagsHandle); return static_cast<jint>(get_canvas(canvasHandle)->saveLayerAlpha(l, t, r, b, alpha, flags)); } -static void restore(JNIEnv* env, jobject, jlong canvasHandle, jboolean throwOnUnderflow) { +static bool restore(jlong canvasHandle) { Canvas* canvas = get_canvas(canvasHandle); - if (canvas->getSaveCount() <= 1) { // cannot restore anymore - if (throwOnUnderflow) { - doThrowISE(env, "Underflow in restore - more restores than saves"); - } - return; // compat behavior - return without throwing + if (canvas->getSaveCount() <= 1) { + return false; // cannot restore anymore } canvas->restore(); + return true; // success } -static void restoreToCount(JNIEnv* env, jobject, jlong canvasHandle, jint restoreCount, - jboolean throwOnUnderflow) { +static void restoreToCount(jlong canvasHandle, jint saveCount) { Canvas* canvas = get_canvas(canvasHandle); - if (restoreCount < 1 || restoreCount > canvas->getSaveCount()) { - if (throwOnUnderflow) { - doThrowIAE(env, "Underflow in restoreToCount - more restores than saves"); - return; - } - restoreCount = 1; // compat behavior - restore as far as possible - } - canvas->restoreToCount(restoreCount); + canvas->restoreToCount(saveCount); } -static void getCTM(JNIEnv* env, jobject, jlong canvasHandle, jlong matrixHandle) { +static jint getSaveCount(jlong canvasHandle) { + return static_cast<jint>(get_canvas(canvasHandle)->getSaveCount()); +} + +static void getMatrix(jlong canvasHandle, jlong matrixHandle) { SkMatrix* matrix = reinterpret_cast<SkMatrix*>(matrixHandle); get_canvas(canvasHandle)->getMatrix(matrix); } -static void setMatrix(JNIEnv* env, jobject, jlong canvasHandle, jlong matrixHandle) { +static void setMatrix(jlong canvasHandle, jlong matrixHandle) { const SkMatrix* matrix = reinterpret_cast<SkMatrix*>(matrixHandle); get_canvas(canvasHandle)->setMatrix(matrix ? *matrix : SkMatrix::I()); } -static void concat(JNIEnv* env, jobject, jlong canvasHandle, jlong matrixHandle) { +static void concat(jlong canvasHandle, jlong matrixHandle) { const SkMatrix* matrix = reinterpret_cast<SkMatrix*>(matrixHandle); get_canvas(canvasHandle)->concat(*matrix); } -static void rotate(JNIEnv*, jobject, jlong canvasHandle, jfloat degrees) { +static void rotate(jlong canvasHandle, jfloat degrees) { get_canvas(canvasHandle)->rotate(degrees); } -static void scale(JNIEnv*, jobject, jlong canvasHandle, jfloat sx, jfloat sy) { +static void scale(jlong canvasHandle, jfloat sx, jfloat sy) { get_canvas(canvasHandle)->scale(sx, sy); } -static void skew(JNIEnv*, jobject, jlong canvasHandle, jfloat sx, jfloat sy) { +static void skew(jlong canvasHandle, jfloat sx, jfloat sy) { get_canvas(canvasHandle)->skew(sx, sy); } -static void translate(JNIEnv*, jobject, jlong canvasHandle, jfloat dx, jfloat dy) { +static void translate(jlong canvasHandle, jfloat dx, jfloat dy) { get_canvas(canvasHandle)->translate(dx, dy); } @@ -171,13 +161,13 @@ static jboolean getClipBounds(JNIEnv* env, jobject, jlong canvasHandle, jobject return result ? JNI_TRUE : JNI_FALSE; } -static jboolean quickRejectRect(JNIEnv* env, jobject, jlong canvasHandle, +static jboolean quickRejectRect(jlong canvasHandle, jfloat left, jfloat top, jfloat right, jfloat bottom) { bool result = get_canvas(canvasHandle)->quickRejectRect(left, top, right, bottom); return result ? JNI_TRUE : JNI_FALSE; } -static jboolean quickRejectPath(JNIEnv* env, jobject, jlong canvasHandle, jlong pathHandle) { +static jboolean quickRejectPath(jlong canvasHandle, jlong pathHandle) { SkPath* path = reinterpret_cast<SkPath*>(pathHandle); bool result = get_canvas(canvasHandle)->quickRejectPath(*path); return result ? JNI_TRUE : JNI_FALSE; @@ -205,14 +195,14 @@ static SkClipOp opHandleToClipOp(jint opHandle) { return static_cast<SkClipOp>(rgnOp); } -static jboolean clipRect(JNIEnv*, jobject, jlong canvasHandle, jfloat l, jfloat t, +static jboolean clipRect(jlong canvasHandle, jfloat l, jfloat t, jfloat r, jfloat b, jint opHandle) { bool nonEmptyClip = get_canvas(canvasHandle)->clipRect(l, t, r, b, opHandleToClipOp(opHandle)); return nonEmptyClip ? JNI_TRUE : JNI_FALSE; } -static jboolean clipPath(JNIEnv* env, jobject, jlong canvasHandle, jlong pathHandle, +static jboolean clipPath(jlong canvasHandle, jlong pathHandle, jint opHandle) { SkPath* path = reinterpret_cast<SkPath*>(pathHandle); bool nonEmptyClip = get_canvas(canvasHandle)->clipPath(path, opHandleToClipOp(opHandle)); @@ -565,7 +555,7 @@ static void drawTextOnPathString(JNIEnv* env, jobject, jlong canvasHandle, jstri env->ReleaseStringChars(text, jchars); } -static void setDrawFilter(JNIEnv* env, jobject, jlong canvasHandle, jlong filterHandle) { +static void setDrawFilter(jlong canvasHandle, jlong filterHandle) { get_canvas(canvasHandle)->setDrawFilter(reinterpret_cast<SkDrawFilter*>(filterHandle)); } @@ -587,6 +577,9 @@ static const JNINativeMethod gMethods[] = { // ------------ @FastNative ---------------- {"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*) CanvasJNI::setBitmap}, + {"nGetClipBounds","(JLandroid/graphics/Rect;)Z", (void*) CanvasJNI::getClipBounds}, + + // ------------ @CriticalNative ---------------- {"nIsOpaque","(J)Z", (void*) CanvasJNI::isOpaque}, {"nGetWidth","(J)I", (void*) CanvasJNI::getWidth}, {"nGetHeight","(J)I", (void*) CanvasJNI::getHeight}, @@ -595,16 +588,15 @@ static const JNINativeMethod gMethods[] = { {"nSaveLayer","(JFFFFJI)I", (void*) CanvasJNI::saveLayer}, {"nSaveLayerAlpha","(JFFFFII)I", (void*) CanvasJNI::saveLayerAlpha}, {"nGetSaveCount","(J)I", (void*) CanvasJNI::getSaveCount}, - {"nRestore","(JZ)V", (void*) CanvasJNI::restore}, - {"nRestoreToCount","(JIZ)V", (void*) CanvasJNI::restoreToCount}, - {"nGetCTM", "(JJ)V", (void*)CanvasJNI::getCTM}, + {"nRestore","(J)Z", (void*) CanvasJNI::restore}, + {"nRestoreToCount","(JI)V", (void*) CanvasJNI::restoreToCount}, + {"nGetMatrix", "(JJ)V", (void*)CanvasJNI::getMatrix}, {"nSetMatrix","(JJ)V", (void*) CanvasJNI::setMatrix}, {"nConcat","(JJ)V", (void*) CanvasJNI::concat}, {"nRotate","(JF)V", (void*) CanvasJNI::rotate}, {"nScale","(JFF)V", (void*) CanvasJNI::scale}, {"nSkew","(JFF)V", (void*) CanvasJNI::skew}, {"nTranslate","(JFF)V", (void*) CanvasJNI::translate}, - {"nGetClipBounds","(JLandroid/graphics/Rect;)Z", (void*) CanvasJNI::getClipBounds}, {"nQuickReject","(JJ)Z", (void*) CanvasJNI::quickRejectPath}, {"nQuickReject","(JFFFF)Z", (void*)CanvasJNI::quickRejectRect}, {"nClipRect","(JFFFFI)Z", (void*) CanvasJNI::clipRect}, diff --git a/core/jni/android_hardware_HardwareBuffer.cpp b/core/jni/android_hardware_HardwareBuffer.cpp new file mode 100644 index 000000000000..74527d97e119 --- /dev/null +++ b/core/jni/android_hardware_HardwareBuffer.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "HardwareBuffer" + +#include "jni.h" +#include "JNIHelp.h" + +#include "android_os_Parcel.h" +#include "android/graphics/GraphicsJNI.h" + +#include <android/hardware_buffer.h> +#include <android_runtime/android_hardware_HardwareBuffer.h> +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> + +#include <binder/Parcel.h> +#include <gui/IGraphicBufferAlloc.h> +#include <gui/ISurfaceComposer.h> +#include <ui/GraphicBuffer.h> + +#include <private/gui/ComposerService.h> + +#include "core_jni_helpers.h" + +using namespace android; + +// ---------------------------------------------------------------------------- +// Defines +// ---------------------------------------------------------------------------- + +// Debug +static const bool kDebugGraphicBuffer = false; + +// ---------------------------------------------------------------------------- +// Types +// ---------------------------------------------------------------------------- + +static struct { + jclass clazz; + jfieldID mNativeObject; + jmethodID ctor; +} gHardwareBufferClassInfo; + +class GraphicBufferWrapper { +public: + explicit GraphicBufferWrapper(const sp<GraphicBuffer>& buffer) + : buffer(buffer) {} + + sp<GraphicBuffer> buffer; +}; + + +// ---------------------------------------------------------------------------- +// Helper functions +// ---------------------------------------------------------------------------- + +static inline bool containsBits(uint64_t mask, uint64_t bitsToCheck) { + return (mask & bitsToCheck) == bitsToCheck; +} + +// ---------------------------------------------------------------------------- +// HardwareBuffer lifecycle +// ---------------------------------------------------------------------------- + +static jlong android_hardware_HardwareBuffer_create(JNIEnv* env, jobject clazz, + jint width, jint height, jint format, jint layers, jlong usage) { + + sp<ISurfaceComposer> composer(ComposerService::getComposerService()); + sp<IGraphicBufferAlloc> alloc(composer->createGraphicBufferAlloc()); + if (alloc == NULL) { + if (kDebugGraphicBuffer) { + ALOGW("createGraphicBufferAlloc() failed in HardwareBuffer.create()"); + } + return NULL; + } + + // TODO: update createGraphicBuffer to take two 64-bit values. + int pixelFormat = android_hardware_HardwareBuffer_convertToPixelFormat(format); + if (pixelFormat == 0) { + if (kDebugGraphicBuffer) { + ALOGW("createGraphicBufferAlloc() invalid pixel format in HardwareBuffer.create()"); + } + return NULL; + } + uint32_t grallocUsage = android_hardware_HardwareBuffer_convertToGrallocUsageBits(usage, 0); + status_t error; + sp<GraphicBuffer> buffer(alloc->createGraphicBuffer(width, height, pixelFormat, + layers, grallocUsage, &error)); + if (buffer == NULL) { + if (kDebugGraphicBuffer) { + ALOGW("createGraphicBuffer() failed in HardwareBuffer.create()"); + } + return NULL; + } + + GraphicBufferWrapper* wrapper = new GraphicBufferWrapper(buffer); + return reinterpret_cast<jlong>(wrapper); +} + +static void destroyWrapper(GraphicBufferWrapper* wrapper) { + delete wrapper; +} + +static jlong android_hardware_HardwareBuffer_getNativeFinalizer(JNIEnv* env, + jobject clazz) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyWrapper)); +} + +//---------------------------------------------------------------------------- +// Accessors +// ---------------------------------------------------------------------------- + +static inline GraphicBuffer* GraphicBufferWrapper_to_GraphicBuffer( + jlong nativeObject) { + return reinterpret_cast<GraphicBufferWrapper*>(nativeObject)->buffer.get(); +} + +static jint android_hardware_HardwareBuffer_getWidth(JNIEnv* env, jobject clazz, + jlong nativeObject) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + return static_cast<jint>(buffer->getWidth()); +} + +static jint android_hardware_HardwareBuffer_getHeight(JNIEnv* env, + jobject clazz, jlong nativeObject) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + return static_cast<jint>(buffer->getHeight()); +} + +static jint android_hardware_HardwareBuffer_getFormat(JNIEnv* env, + jobject clazz, jlong nativeObject) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + return static_cast<jint>(android_hardware_HardwareBuffer_convertFromPixelFormat( + buffer->getPixelFormat())); +} + +static jint android_hardware_HardwareBuffer_getLayers(JNIEnv* env, + jobject clazz, jlong nativeObject) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + return static_cast<jint>(buffer->getLayerCount()); +} + +static jlong android_hardware_HardwareBuffer_getUsage(JNIEnv* env, + jobject clazz, jlong nativeObject) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + return android_hardware_HardwareBuffer_convertFromGrallocUsageBits( + buffer->getUsage()); +} + +// ---------------------------------------------------------------------------- +// Serialization +// ---------------------------------------------------------------------------- + +static void android_hardware_HardwareBuffer_write(JNIEnv* env, jobject clazz, + jlong nativeObject, jobject dest) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject); + Parcel* parcel = parcelForJavaObject(env, dest); + if (parcel) { + parcel->write(*buffer); + } +} + +static jlong android_hardware_HardwareBuffer_read(JNIEnv* env, jobject clazz, + jobject in) { + Parcel* parcel = parcelForJavaObject(env, in); + if (parcel) { + sp<GraphicBuffer> buffer = new GraphicBuffer(); + parcel->read(*buffer); + return reinterpret_cast<jlong>(new GraphicBufferWrapper(buffer)); + } + + return NULL; +} + +// ---------------------------------------------------------------------------- +// Public functions +// ---------------------------------------------------------------------------- + +namespace android { + +AHardwareBuffer* android_hardware_HardwareBuffer_getNativeHardwareBuffer( + JNIEnv* env, jobject hardwareBufferObj) { + if (env->IsInstanceOf(hardwareBufferObj, gHardwareBufferClassInfo.clazz)) { + GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer( + env->GetLongField(hardwareBufferObj, gHardwareBufferClassInfo.mNativeObject)); + return reinterpret_cast<AHardwareBuffer*>(buffer); + } else { + return nullptr; + } +} + +jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer( + JNIEnv* env, AHardwareBuffer* hardwareBuffer) { + GraphicBuffer* buffer = reinterpret_cast<GraphicBuffer*>(hardwareBuffer); + GraphicBufferWrapper* wrapper = new GraphicBufferWrapper(buffer); + jobject hardwareBufferObj = env->NewObject(gHardwareBufferClassInfo.clazz, + gHardwareBufferClassInfo.ctor, reinterpret_cast<jlong>(wrapper)); + if (hardwareBufferObj == NULL) { + delete wrapper; + if (env->ExceptionCheck()) { + ALOGE("Could not create instance of HardwareBuffer from AHardwareBuffer."); + LOGE_EX(env); + env->ExceptionClear(); + } + return nullptr; + } + return hardwareBufferObj; +} + +uint32_t android_hardware_HardwareBuffer_convertFromPixelFormat(uint32_t format) { + switch (format) { + case PIXEL_FORMAT_RGBA_8888: + return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + case PIXEL_FORMAT_RGBX_8888: + return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM; + case PIXEL_FORMAT_RGB_565: + return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; + case PIXEL_FORMAT_RGB_888: + return AHARDWAREBUFFER_FORMAT_R8G8B8_UNORM; + case PIXEL_FORMAT_RGBA_FP16: + return AHARDWAREBUFFER_FORMAT_R16G16B16A16_SFLOAT; + default: + ALOGE("Unknown pixel format %u", format); + return 0; + } +} + +uint32_t android_hardware_HardwareBuffer_convertToPixelFormat(uint32_t format) { + switch (format) { + case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM: + return PIXEL_FORMAT_RGBA_8888; + case AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM: + return PIXEL_FORMAT_RGBX_8888; + case AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM: + return PIXEL_FORMAT_RGB_565; + case AHARDWAREBUFFER_FORMAT_R8G8B8_UNORM: + return PIXEL_FORMAT_RGB_888; + case AHARDWAREBUFFER_FORMAT_R16G16B16A16_SFLOAT: + return PIXEL_FORMAT_RGBA_FP16; + default: + ALOGE("Unknown AHardwareBuffer format %u", format); + return 0; + } +} + +uint32_t android_hardware_HardwareBuffer_convertToGrallocUsageBits(uint64_t usage0, + uint64_t usage1) { + uint32_t bits = 0; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_CPU_READ)) + bits |= GRALLOC_USAGE_SW_READ_RARELY; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_CPU_READ_OFTEN)) + bits |= GRALLOC_USAGE_SW_READ_OFTEN; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_CPU_WRITE)) + bits |= GRALLOC_USAGE_SW_WRITE_RARELY; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_CPU_WRITE_OFTEN)) + bits |= GRALLOC_USAGE_SW_WRITE_OFTEN; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_GPU_SAMPLED_IMAGE)) + bits |= GRALLOC_USAGE_HW_TEXTURE; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_GPU_COLOR_OUTPUT)) + bits |= GRALLOC_USAGE_HW_RENDER; + // Not sure what this should be. + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_GPU_CUBEMAP)) bits |= 0; + //if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_GPU_DATA_BUFFER) bits |= 0; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_VIDEO_ENCODE)) + bits |= GRALLOC_USAGE_HW_VIDEO_ENCODER; + if (containsBits(usage0, AHARDWAREBUFFER_USAGE0_PROTECTED_CONTENT)) + bits |= GRALLOC_USAGE_PROTECTED; + + (void)usage1; + + return bits; +} + +uint64_t android_hardware_HardwareBuffer_convertFromGrallocUsageBits(uint64_t usage0) { + uint64_t bits = 0; + if (containsBits(usage0, GRALLOC_USAGE_SW_READ_RARELY)) + bits |= AHARDWAREBUFFER_USAGE0_CPU_READ; + if (containsBits(usage0, GRALLOC_USAGE_SW_READ_OFTEN)) + bits |= AHARDWAREBUFFER_USAGE0_CPU_READ_OFTEN; + if (containsBits(usage0, GRALLOC_USAGE_SW_WRITE_RARELY)) + bits |= AHARDWAREBUFFER_USAGE0_CPU_WRITE; + if (containsBits(usage0, GRALLOC_USAGE_SW_WRITE_OFTEN)) + bits |= AHARDWAREBUFFER_USAGE0_CPU_WRITE_OFTEN; + if (containsBits(usage0, GRALLOC_USAGE_HW_TEXTURE)) + bits |= AHARDWAREBUFFER_USAGE0_GPU_SAMPLED_IMAGE; + if (containsBits(usage0, GRALLOC_USAGE_HW_RENDER)) + bits |= AHARDWAREBUFFER_USAGE0_GPU_COLOR_OUTPUT; + if (containsBits(usage0, GRALLOC_USAGE_HW_VIDEO_ENCODER)) + bits |= AHARDWAREBUFFER_USAGE0_VIDEO_ENCODE; + if (containsBits(usage0, GRALLOC_USAGE_PROTECTED)) + bits |= AHARDWAREBUFFER_USAGE0_PROTECTED_CONTENT; + + return bits; +} + +} // namespace android + +// ---------------------------------------------------------------------------- +// JNI Glue +// ---------------------------------------------------------------------------- + +const char* const kClassPathName = "android/hardware/HardwareBuffer"; + +static const JNINativeMethod gMethods[] = { + { "nCreateHardwareBuffer", "(IIIIJ)J", (void*) android_hardware_HardwareBuffer_create }, + { "nGetNativeFinalizer", "()J", (void*) android_hardware_HardwareBuffer_getNativeFinalizer }, + { "nWriteHardwareBufferToParcel", "(JLandroid/os/Parcel;)V", + (void*) android_hardware_HardwareBuffer_write }, + { "nReadHardwareBufferFromParcel", "(Landroid/os/Parcel;)J", + (void*) android_hardware_HardwareBuffer_read }, + + // --------------- @FastNative ---------------------- + { "nGetWidth", "(J)I", (void*) android_hardware_HardwareBuffer_getWidth }, + { "nGetHeight", "(J)I", (void*) android_hardware_HardwareBuffer_getHeight }, + { "nGetFormat", "(J)I", (void*) android_hardware_HardwareBuffer_getFormat }, + { "nGetLayers", "(J)I", (void*) android_hardware_HardwareBuffer_getLayers }, + { "nGetUsage", "(J)J", (void*) android_hardware_HardwareBuffer_getUsage }, +}; + +int register_android_hardware_HardwareBuffer(JNIEnv* env) { + int err = RegisterMethodsOrDie(env, kClassPathName, gMethods, + NELEM(gMethods)); + + jclass clazz = FindClassOrDie(env, "android/hardware/HardwareBuffer"); + gHardwareBufferClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gHardwareBufferClassInfo.mNativeObject = GetFieldIDOrDie(env, + gHardwareBufferClassInfo.clazz, "mNativeObject", "J"); + gHardwareBufferClassInfo.ctor = GetMethodIDOrDie(env, + gHardwareBufferClassInfo.clazz, "<init>", "(J)V"); + + return err; +} diff --git a/core/jni/android_util_EventLog.cpp b/core/jni/android_util_EventLog.cpp index 0b4fbcc2e340..8a7600b319cd 100644 --- a/core/jni/android_util_EventLog.cpp +++ b/core/jni/android_util_EventLog.cpp @@ -144,26 +144,22 @@ static jint android_util_EventLog_writeEvent_Array(JNIEnv* env, jobject clazz, return ctx.write(); } -/* - * In class android.util.EventLog: - * static native void readEvents(int[] tags, Collection<Event> output) - * - * Reads events from the event log - */ -static void android_util_EventLog_readEvents(JNIEnv* env, jobject clazz UNUSED, - jintArray tags, - jobject out) { - - if (tags == NULL || out == NULL) { - jniThrowNullPointerException(env, NULL); +static void readEvents(JNIEnv* env, int loggerMode, jintArray tags, jlong startTime, jobject out) { + struct logger_list *logger_list; + if (startTime) { + logger_list = android_logger_list_alloc_time(loggerMode, + log_time(startTime / NS_PER_SEC, startTime % NS_PER_SEC), 0); + } else { + logger_list = android_logger_list_alloc(loggerMode, 0, 0); + } + if (!logger_list) { + jniThrowIOException(env, errno); return; } - struct logger_list *logger_list = android_logger_list_open( - LOG_ID_EVENTS, ANDROID_LOG_RDONLY | ANDROID_LOG_NONBLOCK, 0, 0); - - if (!logger_list) { + if (!android_logger_open(logger_list, LOG_ID_EVENTS)) { jniThrowIOException(env, errno); + android_logger_list_free(logger_list); return; } @@ -228,6 +224,41 @@ static void android_util_EventLog_readEvents(JNIEnv* env, jobject clazz UNUSED, } /* + * In class android.util.EventLog: + * static native void readEvents(int[] tags, Collection<Event> output) + * + * Reads events from the event log + */ +static void android_util_EventLog_readEvents(JNIEnv* env, jobject clazz UNUSED, + jintArray tags, + jobject out) { + + if (tags == NULL || out == NULL) { + jniThrowNullPointerException(env, NULL); + return; + } + + readEvents(env, ANDROID_LOG_RDONLY | ANDROID_LOG_NONBLOCK, tags, 0, out); + } +/* + * In class android.util.EventLog: + * static native void readEventsOnWrapping(int[] tags, long timestamp, Collection<Event> output) + * + * Reads events from the event log, blocking until events after timestamp are to be overwritten. + */ +static void android_util_EventLog_readEventsOnWrapping(JNIEnv* env, jobject clazz UNUSED, + jintArray tags, + jlong timestamp, + jobject out) { + if (tags == NULL || out == NULL) { + jniThrowNullPointerException(env, NULL); + return; + } + readEvents(env, ANDROID_LOG_RDONLY | ANDROID_LOG_NONBLOCK | ANDROID_LOG_WRAP, + tags, timestamp, out); +} + +/* * JNI registration. */ static const JNINativeMethod gRegisterMethods[] = { @@ -247,6 +278,10 @@ static const JNINativeMethod gRegisterMethods[] = { "([ILjava/util/Collection;)V", (void*) android_util_EventLog_readEvents }, + { "readEventsOnWrapping", + "([IJLjava/util/Collection;)V", + (void*) android_util_EventLog_readEventsOnWrapping + }, }; static struct { const char *name; jclass *clazz; } gClasses[] = { diff --git a/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h new file mode 100644 index 000000000000..60e065cbd0e3 --- /dev/null +++ b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ANDROID_HARDWARE_HARDWAREBUFFER_H +#define _ANDROID_HARDWARE_HARDWAREBUFFER_H + +#include <android/hardware_buffer.h> + +#include "jni.h" + +namespace android { + +/* Gets the underlying AHardwareBuffer for a HardwareBuffer. */ +extern AHardwareBuffer* android_hardware_HardwareBuffer_getNativeHardwareBuffer( + JNIEnv* env, jobject hardwareBufferObj); + +/* Returns a HardwareBuffer wrapper for the underlying AHardwareBuffer. */ +extern jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer( + JNIEnv* env, AHardwareBuffer* hardwareBuffer); + +/* Convert from HAL_PIXEL_FORMAT values to AHARDWAREBUFFER_FORMAT values. */ +extern uint32_t android_hardware_HardwareBuffer_convertFromPixelFormat( + uint32_t format); + +/* Convert from AHARDWAREBUFFER_FORMAT values to HAL_PIXEL_FORMAT values. */ +extern uint32_t android_hardware_HardwareBuffer_convertToPixelFormat( + uint32_t format); + +/* Convert from AHARDWAREBUFFER_USAGE* flags to to gralloc usage flags. */ +extern uint32_t android_hardware_HardwareBuffer_convertToGrallocUsageBits( + uint64_t usage0, uint64_t usage1); + +/* Convert from gralloc usage flags to to AHARDWAREBUFFER_USAGE0* flags. */ +extern uint64_t android_hardware_HardwareBuffer_convertFromGrallocUsageBits( + uint64_t usage0); + +} // namespace android + +#endif // _ANDROID_HARDWARE_HARDWAREBUFFER_H diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto index c3b0ff10b2d8..ac9ebe06ae0e 100644 --- a/core/proto/android/os/incident.proto +++ b/core/proto/android/os/incident.proto @@ -22,6 +22,7 @@ option java_outer_classname = "IncidentProtoMetadata"; import "frameworks/base/libs/incident/proto/android/privacy.proto"; import "frameworks/base/core/proto/android/service/fingerprint.proto"; import "frameworks/base/core/proto/android/service/netstats.proto"; +import "frameworks/base/core/proto/android/providers/settings.proto"; package android.os; @@ -51,4 +52,5 @@ message IncidentProto { // System Services android.service.fingerprint.FingerprintServiceDumpProto fingerprint = 3000; android.service.NetworkStatsServiceDumpProto netstats = 3001; + android.providers.settings.SettingsServiceDumpProto settings = 3002; } diff --git a/core/proto/android/providers/settings.proto b/core/proto/android/providers/settings.proto new file mode 100644 index 000000000000..7674d7a524a0 --- /dev/null +++ b/core/proto/android/providers/settings.proto @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package android.providers.settings; + +option java_multiple_files = true; +option java_outer_classname = "SettingsServiceProto"; + +message SettingsServiceDumpProto { + // Per user settings + repeated UserSettingsProto user_settings = 1; + + // Global settings + GlobalSettingsProto global_settings = 2; +} + +message UserSettingsProto { + // Should be 0, 10, 11, 12, etc. where 0 is the owner. + int32 user_id = 1; + + // The secure settings for this user + SecureSettingsProto secure_settings = 2; + + // The system settings for this user + SystemSettingsProto system_settings = 3; +} + +message GlobalSettingsProto { + // Historical operations + repeated SettingsOperationProto historical_op = 1; + + SettingProto add_users_when_locked = 2; + SettingProto enable_accessibility_global_gesture_enabled = 3; + SettingProto airplane_mode_on = 4; + SettingProto theater_mode_on = 5; + SettingProto radio_bluetooth = 6; + SettingProto radio_wifi = 7; + SettingProto radio_wimax = 8; + SettingProto radio_cell = 9; + SettingProto radio_nfc = 10; + SettingProto airplane_mode_radios = 11; + SettingProto airplane_mode_toggleable_radios = 12; + SettingProto bluetooth_disabled_profiles = 13; + SettingProto bluetooth_interoperability_list = 14; + SettingProto wifi_sleep_policy = 15; + SettingProto auto_time = 16; + SettingProto auto_time_zone = 17; + SettingProto car_dock_sound = 18; + SettingProto car_undock_sound = 19; + SettingProto desk_dock_sound = 20; + SettingProto desk_undock_sound = 21; + SettingProto dock_sounds_enabled = 22; + SettingProto dock_sounds_enabled_when_accessibility = 23; + SettingProto lock_sound = 24; + SettingProto unlock_sound = 25; + SettingProto trusted_sound = 26; + SettingProto low_battery_sound = 27; + SettingProto power_sounds_enabled = 28; + SettingProto wireless_charging_started_sound = 29; + SettingProto charging_sounds_enabled = 30; + SettingProto stay_on_while_plugged_in = 31; + SettingProto bugreport_in_power_menu = 32; + SettingProto adb_enabled = 33; + SettingProto debug_view_attributes = 34; + SettingProto assisted_gps_enabled = 35; + SettingProto bluetooth_on = 36; + SettingProto cdma_cell_broadcast_sms = 37; + SettingProto cdma_roaming_mode = 38; + SettingProto cdma_subscription_mode = 39; + SettingProto data_activity_timeout_mobile = 40; + SettingProto data_activity_timeout_wifi = 41; + SettingProto data_roaming = 42; + SettingProto mdc_initial_max_retry = 43; + SettingProto force_allow_on_external = 44; + SettingProto development_force_resizable_activities = 45; + SettingProto development_enable_freeform_windows_support = 46; + SettingProto development_settings_enabled = 47; + SettingProto device_provisioned = 48; + SettingProto device_provisioning_mobile_data_enabled = 49; + SettingProto display_size_forced = 50; + SettingProto display_scaling_force = 51; + SettingProto download_max_bytes_over_mobile = 52; + SettingProto download_recommended_max_bytes_over_mobile = 53; + SettingProto hdmi_control_enabled = 54; + SettingProto hdmi_system_audio_enabled = 55; + SettingProto hdmi_control_auto_wakeup_enabled = 56; + SettingProto hdmi_control_auto_device_off_enabled = 57; + SettingProto mhl_input_switching_enabled = 58; + SettingProto mhl_power_charge_enabled = 59; + SettingProto mobile_data = 60; + SettingProto mobile_data_always_on = 61; + SettingProto connectivity_metrics_buffer_size = 62; + SettingProto netstats_enabled = 63; + SettingProto netstats_poll_interval = 64; + SettingProto netstats_time_cache_max_age = 65; + SettingProto netstats_global_alert_bytes = 66; + SettingProto netstats_sample_enabled = 67; + SettingProto netstats_dev_bucket_duration = 68; + SettingProto netstats_dev_persist_bytes = 69; + SettingProto netstats_dev_rotate_age = 70; + SettingProto netstats_dev_delete_age = 71; + SettingProto netstats_uid_bucket_duration = 72; + SettingProto netstats_uid_persist_bytes = 73; + SettingProto netstats_uid_rotate_age = 74; + SettingProto netstats_uid_delete_age = 75; + SettingProto netstats_uid_tag_bucket_duration = 76; + SettingProto netstats_uid_tag_persist_bytes = 77; + SettingProto netstats_uid_tag_rotate_age = 78; + SettingProto netstats_uid_tag_delete_age = 79; + SettingProto network_preference = 80; + SettingProto network_scorer_app = 81; + SettingProto nitz_update_diff = 82; + SettingProto nitz_update_spacing = 83; + SettingProto ntp_server = 84; + SettingProto ntp_timeout = 85; + SettingProto storage_benchmark_interval = 86; + SettingProto dns_resolver_sample_validity_seconds = 87; + SettingProto dns_resolver_success_threshold_percent = 88; + SettingProto dns_resolver_min_samples = 89; + SettingProto dns_resolver_max_samples = 90; + SettingProto ota_disable_automatic_update = 91; + SettingProto package_verifier_enable = 92; + SettingProto package_verifier_timeout = 93; + SettingProto package_verifier_default_response = 94; + SettingProto package_verifier_setting_visible = 95; + SettingProto package_verifier_include_adb = 96; + SettingProto fstrim_mandatory_interval = 97; + SettingProto pdp_watchdog_poll_interval_ms = 98; + SettingProto pdp_watchdog_long_poll_interval_ms = 99; + SettingProto pdp_watchdog_error_poll_interval_ms = 100; + SettingProto pdp_watchdog_trigger_packet_count = 101; + SettingProto pdp_watchdog_error_poll_count = 102; + SettingProto pdp_watchdog_max_pdp_reset_fail_count = 103; + SettingProto sampling_profiler_ms = 104; + SettingProto setup_prepaid_data_service_url = 105; + SettingProto setup_prepaid_detection_target_url = 106; + SettingProto setup_prepaid_detection_redir_host = 107; + SettingProto sms_outgoing_check_interval_ms = 108; + SettingProto sms_outgoing_check_max_count = 109; + SettingProto sms_short_code_confirmation = 110; + SettingProto sms_short_code_rule = 111; + SettingProto tcp_default_init_rwnd = 112; + SettingProto tether_supported = 113; + SettingProto tether_dun_required = 114; + SettingProto tether_dun_apn = 115; + SettingProto carrier_app_whitelist = 116; + SettingProto usb_mass_storage_enabled = 117; + SettingProto use_google_mail = 118; + SettingProto webview_data_reduction_proxy_key = 119; + SettingProto webview_fallback_logic_enabled = 120; + SettingProto webview_provider = 121; + SettingProto webview_multiprocess = 122; + SettingProto network_switch_notification_daily_limit = 123; + SettingProto network_switch_notification_rate_limit_millis = 124; + SettingProto network_avoid_bad_wifi = 125; + SettingProto wifi_display_on = 126; + SettingProto wifi_display_certification_on = 127; + SettingProto wifi_display_wps_config = 128; + SettingProto wifi_networks_available_notification_on = 129; + SettingProto wimax_networks_available_notification_on = 130; + SettingProto wifi_networks_available_repeat_delay = 131; + SettingProto wifi_country_code = 132; + SettingProto wifi_framework_scan_interval_ms = 133; + SettingProto wifi_idle_ms = 134; + SettingProto wifi_num_open_networks_kept = 135; + SettingProto wifi_on = 136; + SettingProto wifi_scan_always_available = 137; + SettingProto wifi_wakeup_enabled = 138; + SettingProto network_recommendations_enabled = 139; + SettingProto ble_scan_always_available = 140; + SettingProto wifi_saved_state = 141; + SettingProto wifi_supplicant_scan_interval_ms = 142; + SettingProto wifi_enhanced_auto_join = 143; + SettingProto wifi_network_show_rssi = 144; + SettingProto wifi_scan_interval_when_p2p_connected_ms = 145; + SettingProto wifi_watchdog_on = 146; + SettingProto wifi_watchdog_poor_network_test_enabled = 147; + SettingProto wifi_suspend_optimizations_enabled = 148; + SettingProto wifi_verbose_logging_enabled = 149; + SettingProto wifi_max_dhcp_retry_count = 150; + SettingProto wifi_mobile_data_transition_wakelock_timeout_ms = 151; + SettingProto wifi_device_owner_configs_lockdown = 152; + SettingProto wifi_frequency_band = 153; + SettingProto wifi_p2p_device_name = 154; + SettingProto wifi_reenable_delay_ms = 155; + SettingProto wifi_ephemeral_out_of_range_timeout_ms = 156; + SettingProto data_stall_alarm_non_aggressive_delay_in_ms = 157; + SettingProto data_stall_alarm_aggressive_delay_in_ms = 158; + SettingProto provisioning_apn_alarm_delay_in_ms = 159; + SettingProto gprs_register_check_period_ms = 160; + SettingProto wtf_is_fatal = 161; + SettingProto mode_ringer = 162; + SettingProto overlay_display_devices = 163; + SettingProto battery_discharge_duration_threshold = 164; + SettingProto battery_discharge_threshold = 165; + SettingProto send_action_app_error = 166; + SettingProto dropbox_age_seconds = 167; + SettingProto dropbox_max_files = 168; + SettingProto dropbox_quota_kb = 169; + SettingProto dropbox_quota_percent = 170; + SettingProto dropbox_reserve_percent = 171; + SettingProto dropbox_tag_prefix = 172; + SettingProto error_logcat_prefix = 173; + SettingProto sys_free_storage_log_interval = 174; + SettingProto disk_free_change_reporting_threshold = 175; + SettingProto sys_storage_threshold_percentage = 176; + SettingProto sys_storage_threshold_max_bytes = 177; + SettingProto sys_storage_full_threshold_bytes = 178; + SettingProto sync_max_retry_delay_in_seconds = 179; + SettingProto connectivity_change_delay = 180; + SettingProto connectivity_sampling_interval_in_seconds = 181; + SettingProto pac_change_delay = 182; + SettingProto captive_portal_mode = 183; + SettingProto captive_portal_server = 184; + SettingProto captive_portal_https_url = 185; + SettingProto captive_portal_http_url = 186; + SettingProto captive_portal_fallback_url = 187; + SettingProto captive_portal_use_https = 188; + SettingProto captive_portal_user_agent = 189; + SettingProto nsd_on = 190; + SettingProto set_install_location = 191; + SettingProto default_install_location = 192; + SettingProto inet_condition_debounce_up_delay = 193; + SettingProto inet_condition_debounce_down_delay = 194; + SettingProto read_external_storage_enforced_default = 195; + SettingProto http_proxy = 196; + SettingProto global_http_proxy_host = 197; + SettingProto global_http_proxy_port = 198; + SettingProto global_http_proxy_exclusion_list = 199; + SettingProto global_http_proxy_pac = 200; + SettingProto set_global_http_proxy = 201; + SettingProto default_dns_server = 202; + SettingProto bluetooth_headset_priority_prefix = 203; + SettingProto bluetooth_a2dp_sink_priority_prefix = 204; + SettingProto bluetooth_a2dp_src_priority_prefix = 205; + SettingProto bluetooth_input_device_priority_prefix = 206; + SettingProto bluetooth_map_priority_prefix = 207; + SettingProto bluetooth_map_client_priority_prefix = 208; + SettingProto bluetooth_pbap_client_priority_prefix = 209; + SettingProto bluetooth_sap_priority_prefix = 210; + SettingProto bluetooth_pan_priority_prefix = 211; + SettingProto device_idle_constants = 212; + SettingProto device_idle_constants_watch = 213; + SettingProto app_idle_constants = 214; + SettingProto alarm_manager_constants = 215; + SettingProto job_scheduler_constants = 216; + SettingProto shortcut_manager_constants = 217; + SettingProto window_animation_scale = 218; + SettingProto transition_animation_scale = 219; + SettingProto animator_duration_scale = 220; + SettingProto fancy_ime_animations = 221; + SettingProto compatibility_mode = 222; + SettingProto emergency_tone = 223; + SettingProto call_auto_retry = 224; + SettingProto emergency_affordance_needed = 225; + SettingProto preferred_network_mode = 226; + SettingProto debug_app = 227; + SettingProto wait_for_debugger = 228; + SettingProto low_power_mode = 229; + SettingProto low_power_mode_trigger_level = 230; + SettingProto always_finish_activities = 231; + SettingProto dock_audio_media_enabled = 232; + SettingProto encoded_surround_output = 233; + SettingProto audio_safe_volume_state = 234; + SettingProto tzinfo_update_content_url = 235; + SettingProto tzinfo_update_metadata_url = 236; + SettingProto selinux_update_content_url = 237; + SettingProto selinux_update_metadata_url = 238; + SettingProto sms_short_codes_update_content_url = 239; + SettingProto sms_short_codes_update_metadata_url = 240; + SettingProto apn_db_update_content_url = 241; + SettingProto apn_db_update_metadata_url = 242; + SettingProto cert_pin_update_content_url = 243; + SettingProto cert_pin_update_metadata_url = 244; + SettingProto intent_firewall_update_content_url = 245; + SettingProto intent_firewall_update_metadata_url = 246; + SettingProto selinux_status = 247; + SettingProto development_force_rtl = 248; + SettingProto low_battery_sound_timeout = 249; + SettingProto wifi_bounce_delay_override_ms = 250; + SettingProto policy_control = 251; + SettingProto zen_mode = 252; + SettingProto zen_mode_ringer_level = 253; + SettingProto zen_mode_config_etag = 254; + SettingProto heads_up_notifications_enabled = 255; + SettingProto device_name = 256; + SettingProto network_scoring_provisioned = 257; + SettingProto require_password_to_decrypt = 258; + SettingProto enhanced_4g_mode_enabled = 259; + SettingProto vt_ims_enabled = 260; + SettingProto wfc_ims_enabled = 261; + SettingProto wfc_ims_mode = 262; + SettingProto wfc_ims_roaming_mode = 263; + SettingProto wfc_ims_roaming_enabled = 264; + SettingProto lte_service_forced = 265; + SettingProto ephemeral_cookie_max_size_bytes = 266; + SettingProto enable_ephemeral_feature = 267; + SettingProto uninstalled_ephemeral_app_cache_duration_millis = 268; + SettingProto allow_user_switching_when_system_user_locked = 269; + SettingProto boot_count = 270; + SettingProto safe_boot_disallowed = 271; + SettingProto device_demo_mode = 272; + SettingProto retail_demo_mode_constants = 273; + SettingProto database_downgrade_reason = 274; + SettingProto contacts_database_wal_enabled = 275; + SettingProto multi_sim_voice_call_subscription = 276; + SettingProto multi_sim_voice_prompt = 277; + SettingProto multi_sim_data_call_subscription = 278; + SettingProto multi_sim_sms_subscription = 279; + SettingProto multi_sim_sms_prompt = 280; + SettingProto new_contact_aggregator = 281; + SettingProto contact_metadata_sync_enabled = 282; + SettingProto enable_cellular_on_boot = 283; + SettingProto max_notification_enqueue_rate = 284; + SettingProto cell_on = 285; +} + +message SecureSettingsProto { + // Historical operations + repeated SettingsOperationProto historical_op = 1; + + SettingProto android_id = 2; + SettingProto default_input_method = 3; + SettingProto selected_input_method_subtype = 4; + SettingProto input_methods_subtype_history = 5; + SettingProto input_method_selector_visibility = 6; + SettingProto voice_interaction_service = 7; + SettingProto auto_fill_service = 8; + SettingProto bluetooth_hci_log = 9; + SettingProto user_setup_complete = 10; + SettingProto completed_category_prefix = 11; + SettingProto enabled_input_methods = 12; + SettingProto disabled_system_input_methods = 13; + SettingProto show_ime_with_hard_keyboard = 14; + SettingProto always_on_vpn_app = 15; + SettingProto always_on_vpn_lockdown = 16; + SettingProto install_non_market_apps = 17; + SettingProto location_mode = 18; + SettingProto location_previous_mode = 19; + SettingProto lock_to_app_exit_locked = 20; + SettingProto lock_screen_lock_after_timeout = 21; + SettingProto lock_screen_allow_remote_input = 22; + SettingProto show_note_about_notification_hiding = 23; + SettingProto trust_agents_initialized = 24; + SettingProto parental_control_enabled = 25; + SettingProto parental_control_last_update = 26; + SettingProto parental_control_redirect_url = 27; + SettingProto settings_classname = 28; + SettingProto accessibility_enabled = 29; + SettingProto touch_exploration_enabled = 30; + SettingProto enabled_accessibility_services = 31; + SettingProto touch_exploration_granted_accessibility_services = 32; + SettingProto accessibility_speak_password = 33; + SettingProto accessibility_high_text_contrast_enabled = 34; + SettingProto accessibility_script_injection = 35; + SettingProto accessibility_screen_reader_url = 36; + SettingProto accessibility_web_content_key_bindings = 37; + SettingProto accessibility_display_magnification_enabled = 38; + SettingProto accessibility_display_magnification_scale = 39; + SettingProto accessibility_soft_keyboard_mode = 40; + SettingProto accessibility_captioning_enabled = 41; + SettingProto accessibility_captioning_locale = 42; + SettingProto accessibility_captioning_preset = 43; + SettingProto accessibility_captioning_background_color = 44; + SettingProto accessibility_captioning_foreground_color = 45; + SettingProto accessibility_captioning_edge_type = 46; + SettingProto accessibility_captioning_edge_color = 47; + SettingProto accessibility_captioning_window_color = 48; + SettingProto accessibility_captioning_typeface = 49; + SettingProto accessibility_captioning_font_scale = 50; + SettingProto accessibility_display_inversion_enabled = 51; + SettingProto accessibility_display_daltonizer_enabled = 52; + SettingProto accessibility_display_daltonizer = 53; + SettingProto accessibility_autoclick_enabled = 54; + SettingProto accessibility_autoclick_delay = 55; + SettingProto accessibility_large_pointer_icon = 56; + SettingProto long_press_timeout = 57; + SettingProto multi_press_timeout = 58; + SettingProto enabled_print_services = 59; + SettingProto disabled_print_services = 60; + SettingProto display_density_forced = 61; + SettingProto tts_default_rate = 62; + SettingProto tts_default_pitch = 63; + SettingProto tts_default_synth = 64; + SettingProto tts_default_locale = 65; + SettingProto tts_enabled_plugins = 66; + SettingProto connectivity_release_pending_intent_delay_ms = 67; + SettingProto allowed_geolocation_origins = 68; + SettingProto preferred_tty_mode = 69; + SettingProto enhanced_voice_privacy_enabled = 70; + SettingProto tty_mode_enabled = 71; + SettingProto backup_enabled = 72; + SettingProto backup_auto_restore = 73; + SettingProto backup_provisioned = 74; + SettingProto backup_transport = 75; + SettingProto last_setup_shown = 76; + SettingProto search_global_search_activity = 77; + SettingProto search_num_promoted_sources = 78; + SettingProto search_max_results_to_display = 79; + SettingProto search_max_results_per_source = 80; + SettingProto search_web_results_override_limit = 81; + SettingProto search_promoted_source_deadline_millis = 82; + SettingProto search_source_timeout_millis = 83; + SettingProto search_prefill_millis = 84; + SettingProto search_max_stat_age_millis = 85; + SettingProto search_max_source_event_age_millis = 86; + SettingProto search_min_impressions_for_source_ranking = 87; + SettingProto search_min_clicks_for_source_ranking = 88; + SettingProto search_max_shortcuts_returned = 89; + SettingProto search_query_thread_core_pool_size = 90; + SettingProto search_query_thread_max_pool_size = 91; + SettingProto search_shortcut_refresh_core_pool_size = 92; + SettingProto search_shortcut_refresh_max_pool_size = 93; + SettingProto search_thread_keepalive_seconds = 94; + SettingProto search_per_source_concurrent_query_limit = 95; + SettingProto mount_play_notification_snd = 96; + SettingProto mount_ums_autostart = 97; + SettingProto mount_ums_prompt = 98; + SettingProto mount_ums_notify_enabled = 99; + SettingProto anr_show_background = 100; + SettingProto voice_recognition_service = 101; + SettingProto package_verifier_user_consent = 102; + SettingProto selected_spell_checker = 103; + SettingProto selected_spell_checker_subtype = 104; + SettingProto spell_checker_enabled = 105; + SettingProto incall_power_button_behavior = 106; + SettingProto incall_back_button_behavior = 107; + SettingProto wake_gesture_enabled = 108; + SettingProto doze_enabled = 109; + SettingProto doze_always_on = 110; + SettingProto doze_pulse_on_pick_up = 111; + SettingProto doze_pulse_on_double_tap = 112; + SettingProto ui_night_mode = 113; + SettingProto screensaver_enabled = 114; + SettingProto screensaver_components = 115; + SettingProto screensaver_activate_on_dock = 116; + SettingProto screensaver_activate_on_sleep = 117; + SettingProto screensaver_default_component = 118; + SettingProto nfc_payment_default_component = 119; + SettingProto nfc_payment_foreground = 120; + SettingProto sms_default_application = 121; + SettingProto dialer_default_application = 122; + SettingProto emergency_assistance_application = 123; + SettingProto assist_structure_enabled = 124; + SettingProto assist_screenshot_enabled = 125; + SettingProto assist_disclosure_enabled = 126; + SettingProto enabled_notification_assistant = 127; + SettingProto enabled_notification_listeners = 128; + SettingProto enabled_notification_policy_access_packages = 129; + SettingProto sync_parent_sounds = 130; + SettingProto immersive_mode_confirmations = 131; + SettingProto print_service_search_uri = 132; + SettingProto payment_service_search_uri = 133; + SettingProto skip_first_use_hints = 134; + SettingProto unsafe_volume_music_active_ms = 135; + SettingProto lock_screen_show_notifications = 136; + SettingProto tv_input_hidden_inputs = 137; + SettingProto tv_input_custom_labels = 138; + SettingProto usb_audio_automatic_routing_disabled = 139; + SettingProto sleep_timeout = 140; + SettingProto double_tap_to_wake = 141; + SettingProto assistant = 142; + SettingProto camera_gesture_disabled = 143; + SettingProto camera_double_tap_power_gesture_disabled = 144; + SettingProto camera_double_twist_to_flip_enabled = 145; + SettingProto night_display_activated = 146; + SettingProto night_display_auto_mode = 147; + SettingProto night_display_custom_start_time = 148; + SettingProto night_display_custom_end_time = 149; + SettingProto brightness_use_twilight = 150; + SettingProto enabled_vr_listeners = 151; + SettingProto vr_display_mode = 152; + SettingProto carrier_apps_handled = 153; + SettingProto managed_profile_contact_remote_search = 154; + SettingProto automatic_storage_manager_enabled = 155; + SettingProto automatic_storage_manager_days_to_retain = 156; + SettingProto automatic_storage_manager_bytes_cleared = 157; + SettingProto automatic_storage_manager_last_run = 158; + SettingProto system_navigation_keys_enabled = 159; + SettingProto downloads_backup_enabled = 160; + SettingProto downloads_backup_allow_metered = 161; + SettingProto downloads_backup_charging_only = 162; + SettingProto automatic_storage_manager_downloads_days_to_retain = 163; + SettingProto qs_tiles = 164; + SettingProto demo_user_setup_complete = 165; + SettingProto web_action_enabled = 166; + SettingProto device_paired = 167; +} + +message SystemSettingsProto { + // Historical operations + repeated SettingsOperationProto historical_op = 1; + + SettingProto end_button_behavior = 2; + SettingProto advanced_settings = 3; + SettingProto bluetooth_discoverability = 4; + SettingProto bluetooth_discoverability_timeout = 5; + SettingProto font_scale = 6; + SettingProto system_locales = 7; + SettingProto screen_off_timeout = 8; + SettingProto screen_brightness = 9; + SettingProto screen_brightness_for_vr = 10; + SettingProto screen_brightness_mode = 11; + SettingProto screen_auto_brightness_adj = 12; + SettingProto mode_ringer_streams_affected = 13; + SettingProto mute_streams_affected = 14; + SettingProto vibrate_on = 15; + SettingProto vibrate_input_devices = 16; + SettingProto volume_ring = 17; + SettingProto volume_system = 18; + SettingProto volume_voice = 19; + SettingProto volume_music = 20; + SettingProto volume_alarm = 21; + SettingProto volume_notification = 22; + SettingProto volume_bluetooth_sco = 23; + SettingProto volume_master = 24; + SettingProto master_mono = 25; + SettingProto vibrate_in_silent = 26; + SettingProto append_for_last_audible = 27; + SettingProto ringtone = 28; + SettingProto ringtone_cache = 29; + SettingProto notification_sound = 30; + SettingProto notification_sound_cache = 31; + SettingProto alarm_alert = 32; + SettingProto alarm_alert_cache = 33; + SettingProto media_button_receiver = 34; + SettingProto text_auto_replace = 35; + SettingProto text_auto_caps = 36; + SettingProto text_auto_punctuate = 37; + SettingProto text_show_password = 38; + SettingProto show_gtalk_service_status = 39; + SettingProto time_12_24 = 40; + SettingProto date_format = 41; + SettingProto setup_wizard_has_run = 42; + SettingProto accelerometer_rotation = 43; + SettingProto user_rotation = 44; + SettingProto hide_rotation_lock_toggle_for_accessibility = 45; + SettingProto vibrate_when_ringing = 46; + SettingProto dtmf_tone_when_dialing = 47; + SettingProto dtmf_tone_type_when_dialing = 48; + SettingProto hearing_aid = 49; + SettingProto tty_mode = 50; + SettingProto sound_effects_enabled = 51; + SettingProto haptic_feedback_enabled = 52; + SettingProto notification_light_pulse = 53; + SettingProto pointer_location = 54; + SettingProto show_touches = 55; + SettingProto window_orientation_listener_log = 56; + SettingProto lockscreen_sounds_enabled = 57; + SettingProto lockscreen_disabled = 58; + SettingProto sip_receive_calls = 59; + SettingProto sip_call_options = 60; + SettingProto sip_always = 61; + SettingProto sip_address_only = 62; + SettingProto pointer_speed = 63; + SettingProto lock_to_app_enabled = 64; + SettingProto egg_mode = 65; + SettingProto when_to_make_wifi_calls = 66; +} + +message SettingProto { + // ID of the setting + string id = 1; + + // Name of the setting + string name = 2; + + // Package name of the setting + string pkg = 3; + + // Value of this setting + string value = 4; + + // Default value of this setting + string default_value = 5; + + // Whether the default is set by the system + bool default_from_system = 6; +} + +message SettingsOperationProto { + // When the operation happened + int64 timestamp = 1; + + // Type of the operation + string operation = 2; + + // Name of the setting that was affected (optional) + string setting = 3; +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 2692bf2bce42..6d48862ea132 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1963,7 +1963,7 @@ {@link android.content.pm.PackageManager#addPackageToPreferred} for details. --> <permission android:name="android.permission.SET_PREFERRED_APPLICATIONS" - android:protectionLevel="signature" /> + android:protectionLevel="signature|verifier" /> <!-- Allows an application to receive the {@link android.content.Intent#ACTION_BOOT_COMPLETED} that is diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 7c01dea63cee..df7a5f523caa 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2896,9 +2896,7 @@ See {@link android.view.View#setKeyboardNavigationCluster(boolean)}. --> <attr name="keyboardNavigationCluster" format="boolean" /> - <!-- Whether this view is a root of a keyboard navigation section. - See {@link android.view.View#setKeyboardNavigationSection(boolean)}. --> - <attr name="keyboardNavigationSection" format="boolean" /> + <attr name="__removed0" format="boolean" /> <!-- Defines the next keyboard navigation cluster. @@ -2907,12 +2905,7 @@ will result when the reference is accessed.--> <attr name="nextClusterForward" format="reference"/> - <!-- Defines the next keyboard navigation section. - - If the reference refers to a view that does not exist or is part - of a hierarchy that is invisible, a {@link java.lang.RuntimeException} - will result when the reference is accessed.--> - <attr name="nextSectionForward" format="reference"/> + <attr name="__removed1" format="reference"/> <!-- Whether this view is a default-focus view. Only one view per keyboard navigation cluster can have this attribute set to true. @@ -8477,4 +8470,14 @@ <attr name="font" format="reference" /> <attr name="fontWeight" format="integer" /> </declare-styleable> + + <!-- @hide --> + <declare-styleable name="RecyclerView"> + <attr name="layoutManager" format="string" /> + <attr name="orientation" /> + <attr name="descendantFocusability" /> + <attr name="spanCount" format="integer"/> + <attr name="reverseLayout" format="boolean" /> + <attr name="stackFromEnd" format="boolean" /> + </declare-styleable> </resources> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index bd195216ae4e..e6358a37bded 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -509,4 +509,10 @@ <dimen name="tooltip_precise_anchor_threshold">96dp</dimen> <!-- Extra tooltip offset used when anchoring to the mouse/touch position --> <dimen name="tooltip_precise_anchor_extra_offset">8dp</dimen> + + <!-- The max amount of scroll ItemTouchHelper will trigger if dragged view is out of + RecyclerView's bounds.--> + <dimen name="item_touch_helper_max_drag_scroll_per_frame">20dp</dimen> + <dimen name="item_touch_helper_swipe_escape_velocity">120dp</dimen> + <dimen name="item_touch_helper_swipe_escape_max_velocity">800dp</dimen> </resources> diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml index 55477063a71a..f351b7008547 100644 --- a/core/res/res/values/ids.xml +++ b/core/res/res/values/ids.xml @@ -131,4 +131,7 @@ <item type="id" name="cross_task_transition" /> <item type="id" name="accessibilityActionClickOnClickableSpan" /> + + <!-- ItemTouchHelper uses this id to save a View's original elevation. --> + <item type="id" name="item_touch_helper_previous_elevation"/> </resources> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 060c59ed4a31..40d0e45e7d3b 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2777,9 +2777,9 @@ <public name="paddingVertical" /> <public name="visibleToInstantApps" /> <public name="keyboardNavigationCluster" /> - <public name="keyboardNavigationSection" /> + <public name="__removed0" /> <public name="nextClusterForward" /> - <public name="nextSectionForward" /> + <public name="__removed1" /> <public name="textColorError" /> <public name="focusedByDefault" /> <public name="appCategory" /> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c370ef74f6a9..6eb3bee67741 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2806,4 +2806,10 @@ <java-symbol type="string" name="disable_accessibility_shortcut" /> <java-symbol type="string" name="leave_accessibility_shortcut_on" /> <java-symbol type="string" name="config_defaultAccessibilityService" /> + + <!-- com.android.internal.widget.RecyclerView --> + <java-symbol type="id" name="item_touch_helper_previous_elevation"/> + <java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/> + <java-symbol type="dimen" name="item_touch_helper_swipe_escape_velocity"/> + <java-symbol type="dimen" name="item_touch_helper_swipe_escape_max_velocity"/> </resources> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index cba485a606cc..91ce7a468484 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1168,6 +1168,7 @@ <activity android:name="android.app.EmptyActivity"> </activity> <activity android:name="com.android.internal.app.ChooserWrapperActivity"/> + <activity android:name="com.android.internal.app.ResolverWrapperActivity"/> <receiver android:name="android.app.activity.AbortReceiver"> <intent-filter android:priority="1"> diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index aab4698fc49a..daebf888f715 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -222,7 +222,7 @@ public class ChooserActivityTest { private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ChooserDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); } return infoList; } diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index 41016e12bbf9..c446f3c79ea8 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -24,11 +24,10 @@ import java.util.function.Function; import static org.mockito.Mockito.mock; - -/** - * Simple wrapper around chooser activity to be able to initiate it under test - */ public class ChooserWrapperActivity extends ChooserActivity { + /* + * Simple wrapper around chooser activity to be able to initiate it under test + */ static final OverrideData sOverrides = new OverrideData(); private UsageStatsManager mUsm; @@ -94,4 +93,4 @@ public class ChooserWrapperActivity extends ChooserActivity { resolverListController = mock(ResolverListController.class); } } -}
\ No newline at end of file +} diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java new file mode 100644 index 000000000000..84b844a5b4b6 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 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.internal.app; + +import com.android.internal.R; +import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static android.os.SystemClock.sleep; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static com.android.internal.app.ResolverWrapperActivity.sOverrides; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Resolver activity instrumentation tests + */ +@RunWith(AndroidJUnit4.class) +public class ResolverActivityTest { + @Rule + public ActivityTestRule<ResolverWrapperActivity> mActivityRule = + new ActivityTestRule<>(ResolverWrapperActivity.class, false, + false); + + @Before + public void cleanOverrideData() { + sOverrides.reset(); + } + + @Test + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasLastChosenActivity() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withId(R.id.title)).perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + private Intent createSendImageIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("image/jpeg"); + return sendIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + return infoList; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java index f6f63f1de81e..ae063065afad 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java @@ -22,16 +22,15 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.os.UserHandle; -import android.service.chooser.ChooserTarget; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** - * Utility class used by chooser tests to create mock data + * Utility class used by resolver tests to create mock data */ -class ChooserDataProvider { +class ResolverDataProvider { static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo(int i) { return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java new file mode 100644 index 000000000000..163211e10e6a --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.RemoteException; + +import java.util.function.Function; + +import static org.mockito.Mockito.mock; + + +/* + * Simple wrapper around chooser activity to be able to initiate it under test + */ +public class ResolverWrapperActivity extends ResolverActivity { + static final OverrideData sOverrides = new OverrideData(); + private UsageStatsManager mUsm; + + ResolveListAdapter getAdapter() { + return mAdapter; + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + public void safelyStartActivity(TargetInfo cti) { + if (sOverrides.onSafelyStartCallback != null && + sOverrides.onSafelyStartCallback.apply(cti)) { + return; + } + super.safelyStartActivity(cti); + } + + @Override + protected ResolverListController createListController() { + return sOverrides.resolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + /** + * We cannot directly mock the activity created since instrumentation creates it. + * <p> + * Instead, we use static instances of this object to modify behavior. + */ + static class OverrideData { + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<TargetInfo, Boolean> onSafelyStartCallback; + public ResolverListController resolverListController; + public Boolean isVoiceInteraction; + + public void reset() { + onSafelyStartCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + resolverListController = mock(ResolverListController.class); + } + } +}
\ No newline at end of file diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index cc5cc7bd8cf8..85723456399a 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -21,11 +21,8 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; -import android.text.GraphicsOperations; -import android.text.SpannableString; -import android.text.SpannedString; -import android.text.TextUtils; +import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; @@ -501,8 +498,10 @@ public class Canvas extends BaseCanvas { * an error to call restore() more times than save() was called. */ public void restore() { - boolean throwOnUnderflow = !sCompatibilityRestore || !isHardwareAccelerated(); - nRestore(mNativeCanvasWrapper, throwOnUnderflow); + if (!nRestore(mNativeCanvasWrapper) + && (!sCompatibilityRestore || !isHardwareAccelerated())) { + throw new IllegalStateException("Underflow in restore - more restores than saves"); + } } /** @@ -527,8 +526,16 @@ public class Canvas extends BaseCanvas { * @param saveCount The save level to restore to. */ public void restoreToCount(int saveCount) { - boolean throwOnUnderflow = !sCompatibilityRestore || !isHardwareAccelerated(); - nRestoreToCount(mNativeCanvasWrapper, saveCount, throwOnUnderflow); + if (saveCount < 1) { + if (!sCompatibilityRestore || !isHardwareAccelerated()) { + // do nothing and throw without restoring + throw new IllegalArgumentException( + "Underflow in restoreToCount - more restores than saves"); + } + // compat behavior - restore as far as possible + saveCount = 1; + } + nRestoreToCount(mNativeCanvasWrapper, saveCount); } /** @@ -643,7 +650,7 @@ public class Canvas extends BaseCanvas { */ @Deprecated public void getMatrix(@NonNull Matrix ctm) { - nGetCTM(mNativeCanvasWrapper, ctm.native_instance); + nGetMatrix(mNativeCanvasWrapper, ctm.native_instance); } /** @@ -1059,79 +1066,66 @@ public class Canvas extends BaseCanvas { // ---------------- @FastNative ------------------- @FastNative - private static native void nSetBitmap(long canvasHandle, - Bitmap bitmap); + private static native void nSetBitmap(long canvasHandle, Bitmap bitmap); + @FastNative + private static native boolean nGetClipBounds(long nativeCanvas, Rect bounds); + + // ---------------- @CriticalNative ------------------- + + @CriticalNative private static native boolean nIsOpaque(long canvasHandle); - @FastNative + @CriticalNative private static native void nSetHighContrastText(long renderer, boolean highContrastText); - @FastNative + @CriticalNative private static native int nGetWidth(long canvasHandle); - @FastNative + @CriticalNative private static native int nGetHeight(long canvasHandle); - @FastNative + @CriticalNative private static native int nSave(long canvasHandle, int saveFlags); - @FastNative - private static native int nSaveLayer(long nativeCanvas, float l, - float t, float r, float b, - long nativePaint, - int layerFlags); - @FastNative - private static native int nSaveLayerAlpha(long nativeCanvas, float l, - float t, float r, float b, - int alpha, int layerFlags); - @FastNative - private static native void nRestore(long canvasHandle, boolean tolerateUnderflow); - @FastNative - private static native void nRestoreToCount(long canvasHandle, - int saveCount, - boolean tolerateUnderflow); - @FastNative + @CriticalNative + private static native int nSaveLayer(long nativeCanvas, float l, float t, float r, float b, + long nativePaint, int layerFlags); + @CriticalNative + private static native int nSaveLayerAlpha(long nativeCanvas, float l, float t, float r, float b, + int alpha, int layerFlags); + @CriticalNative + private static native boolean nRestore(long canvasHandle); + @CriticalNative + private static native void nRestoreToCount(long canvasHandle, int saveCount); + @CriticalNative private static native int nGetSaveCount(long canvasHandle); - @FastNative - private static native void nTranslate(long canvasHandle, - float dx, float dy); - @FastNative - private static native void nScale(long canvasHandle, - float sx, float sy); - @FastNative + @CriticalNative + private static native void nTranslate(long canvasHandle, float dx, float dy); + @CriticalNative + private static native void nScale(long canvasHandle, float sx, float sy); + @CriticalNative private static native void nRotate(long canvasHandle, float degrees); - @FastNative - private static native void nSkew(long canvasHandle, - float sx, float sy); - @FastNative - private static native void nConcat(long nativeCanvas, - long nativeMatrix); - @FastNative - private static native void nSetMatrix(long nativeCanvas, - long nativeMatrix); - @FastNative + @CriticalNative + private static native void nSkew(long canvasHandle, float sx, float sy); + @CriticalNative + private static native void nConcat(long nativeCanvas, long nativeMatrix); + @CriticalNative + private static native void nSetMatrix(long nativeCanvas, long nativeMatrix); + @CriticalNative private static native boolean nClipRect(long nativeCanvas, - float left, float top, - float right, float bottom, - int regionOp); - @FastNative - private static native boolean nClipPath(long nativeCanvas, - long nativePath, - int regionOp); - @FastNative - private static native void nSetDrawFilter(long nativeCanvas, - long nativeFilter); - @FastNative - private static native boolean nGetClipBounds(long nativeCanvas, - Rect bounds); - @FastNative - private static native void nGetCTM(long nativeCanvas, - long nativeMatrix); - @FastNative - private static native boolean nQuickReject(long nativeCanvas, - long nativePath); - @FastNative - private static native boolean nQuickReject(long nativeCanvas, - float left, float top, - float right, float bottom); + float left, float top, float right, float bottom, int regionOp); + @CriticalNative + private static native boolean nClipPath(long nativeCanvas, long nativePath, int regionOp); + @CriticalNative + private static native void nSetDrawFilter(long nativeCanvas, long nativeFilter); + @CriticalNative + private static native void nGetMatrix(long nativeCanvas, long nativeMatrix); + @CriticalNative + private static native boolean nQuickReject(long nativeCanvas, long nativePath); + @CriticalNative + private static native boolean nQuickReject(long nativeCanvas, float left, float top, + float right, float bottom); + + + // ---------------- Draw Methods ------------------- /** * <p> diff --git a/graphics/java/android/graphics/FontFamily.java b/graphics/java/android/graphics/FontFamily.java index cd5071e87e9e..8673e0bb6a9c 100644 --- a/graphics/java/android/graphics/FontFamily.java +++ b/graphics/java/android/graphics/FontFamily.java @@ -17,6 +17,7 @@ package android.graphics; import android.content.res.AssetManager; +import android.text.FontConfig; import android.util.Log; import java.io.FileInputStream; @@ -80,7 +81,7 @@ public class FontFamily { } } - public boolean addFontWeightStyle(ByteBuffer font, int ttcIndex, List<FontListParser.Axis> axes, + public boolean addFontWeightStyle(ByteBuffer font, int ttcIndex, List<FontConfig.Axis> axes, int weight, boolean style) { return nAddFontWeightStyle(mNativePtr, font, ttcIndex, axes, weight, style); } @@ -94,7 +95,7 @@ public class FontFamily { private static native void nUnrefFamily(long nativePtr); private static native boolean nAddFont(long nativeFamily, ByteBuffer font, int ttcIndex); private static native boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font, - int ttcIndex, List<FontListParser.Axis> listOfAxis, + int ttcIndex, List<FontConfig.Axis> listOfAxis, int weight, boolean isItalic); private static native boolean nAddFontFromAssetManager(long nativeFamily, AssetManager mgr, String path, int cookie, boolean isAsset); diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index 9490436d4f04..4ec564ac5809 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -16,6 +16,7 @@ package android.graphics; +import android.text.FontConfig; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; @@ -36,61 +37,8 @@ import java.util.regex.Pattern; */ public class FontListParser { - public static class Config { - Config() { - families = new ArrayList<Family>(); - aliases = new ArrayList<Alias>(); - } - public List<Family> families; - public List<Alias> aliases; - } - - public static class Axis { - Axis(int tag, float styleValue) { - this.tag = tag; - this.styleValue = styleValue; - } - public final int tag; - public final float styleValue; - } - - public static class Font { - Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) { - this.fontName = fontName; - this.ttcIndex = ttcIndex; - this.axes = axes; - this.weight = weight; - this.isItalic = isItalic; - } - public String fontName; - public int ttcIndex; - public final List<Axis> axes; - public int weight; - public boolean isItalic; - } - - public static class Alias { - public String name; - public String toName; - public int weight; - } - - public static class Family { - public Family(String name, List<Font> fonts, String lang, String variant) { - this.name = name; - this.fonts = fonts; - this.lang = lang; - this.variant = variant; - } - - public String name; - public List<Font> fonts; - public String lang; - public String variant; - } - /* Parse fallback list (no names) */ - public static Config parse(InputStream in) throws XmlPullParserException, IOException { + public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); @@ -104,9 +52,9 @@ public class FontListParser { // Note that a well-formed variation contains a four-character tag and a float as styleValue, // with spacers in between. The tag is enclosd either by double quotes or single quotes. @VisibleForTesting - public static Axis[] parseFontVariationSettings(String settings) { + public static FontConfig.Axis[] parseFontVariationSettings(String settings) { String[] settingList = settings.split(","); - ArrayList<Axis> axisList = new ArrayList<>(); + ArrayList<FontConfig.Axis> axisList = new ArrayList<>(); settingLoop: for (String setting : settingList) { int pos = 0; @@ -148,9 +96,9 @@ public class FontListParser { } int tag = makeTag(tagString.charAt(0), tagString.charAt(1), tagString.charAt(2), tagString.charAt(3)); - axisList.add(new Axis(tag, styleValue)); + axisList.add(new FontConfig.Axis(tag, styleValue)); } - return axisList.toArray(new Axis[axisList.size()]); + return axisList.toArray(new FontConfig.Axis[axisList.size()]); } @VisibleForTesting @@ -162,17 +110,17 @@ public class FontListParser { return c == ' ' || c == '\r' || c == '\t' || c == '\n'; } - private static Config readFamilies(XmlPullParser parser) + private static FontConfig readFamilies(XmlPullParser parser) throws XmlPullParserException, IOException { - Config config = new Config(); + FontConfig config = new FontConfig(); parser.require(XmlPullParser.START_TAG, null, "familyset"); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("family")) { - config.families.add(readFamily(parser)); + config.getFamilies().add(readFamily(parser)); } else if (tag.equals("alias")) { - config.aliases.add(readAlias(parser)); + config.getAliases().add(readAlias(parser)); } else { skip(parser); } @@ -180,12 +128,12 @@ public class FontListParser { return config; } - private static Family readFamily(XmlPullParser parser) + private static FontConfig.Family readFamily(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue(null, "name"); String lang = parser.getAttributeValue(null, "lang"); String variant = parser.getAttributeValue(null, "variant"); - List<Font> fonts = new ArrayList<Font>(); + List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); @@ -195,18 +143,18 @@ public class FontListParser { skip(parser); } } - return new Family(name, fonts, lang, variant); + return new FontConfig.Family(name, fonts, lang, variant); } /** Matches leading and trailing XML whitespace. */ private static final Pattern FILENAME_WHITESPACE_PATTERN = Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); - private static Font readFont(XmlPullParser parser) + private static FontConfig.Font readFont(XmlPullParser parser) throws XmlPullParserException, IOException { String indexStr = parser.getAttributeValue(null, "index"); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); - List<Axis> axes = new ArrayList<Axis>(); + List<FontConfig.Axis> axes = new ArrayList<FontConfig.Axis>(); String weightStr = parser.getAttributeValue(null, "weight"); int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style")); @@ -225,7 +173,7 @@ public class FontListParser { } String fullFilename = "/system/fonts/" + FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); - return new Font(fullFilename, index, axes, weight, isItalic); + return new FontConfig.Font(fullFilename, index, axes, weight, isItalic); } /** The 'tag' attribute value is read as four character values between U+0020 and U+007E @@ -239,7 +187,7 @@ public class FontListParser { private static final Pattern STYLE_VALUE_PATTERN = Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))"); - private static Axis readAxis(XmlPullParser parser) + private static FontConfig.Axis readAxis(XmlPullParser parser) throws XmlPullParserException, IOException { int tag = 0; String tagStr = parser.getAttributeValue(null, "tag"); @@ -258,22 +206,22 @@ public class FontListParser { } skip(parser); // axis tag is empty, ignore any contents and consume end tag - return new Axis(tag, styleValue); + return new FontConfig.Axis(tag, styleValue); } - private static Alias readAlias(XmlPullParser parser) + private static FontConfig.Alias readAlias(XmlPullParser parser) throws XmlPullParserException, IOException { - Alias alias = new Alias(); - alias.name = parser.getAttributeValue(null, "name"); - alias.toName = parser.getAttributeValue(null, "to"); + String name = parser.getAttributeValue(null, "name"); + String toName = parser.getAttributeValue(null, "to"); String weightStr = parser.getAttributeValue(null, "weight"); + int weight; if (weightStr == null) { - alias.weight = 400; + weight = 400; } else { - alias.weight = Integer.parseInt(weightStr); + weight = Integer.parseInt(weightStr); } skip(parser); // alias tag is empty, ignore any contents and consume end tag - return alias; + return new FontConfig.Alias(name, toName, weight); } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { diff --git a/graphics/java/android/graphics/FontResourcesParser.java b/graphics/java/android/graphics/FontResourcesParser.java deleted file mode 100644 index b4109cfa3296..000000000000 --- a/graphics/java/android/graphics/FontResourcesParser.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.graphics; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Parser for xml type font resources. - * @hide - */ -public class FontResourcesParser { - private static final String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"; - - /* Parse fallback list (no names) */ - public static FontListParser.Config parse(XmlPullParser parser) - throws XmlPullParserException, IOException { - int type; - //noinspection StatementWithEmptyBody - while ((type=parser.next()) != XmlPullParser.START_TAG - && type != XmlPullParser.END_DOCUMENT) { - // Empty loop. - } - - if (type != XmlPullParser.START_TAG) { - throw new XmlPullParserException("No start tag found"); - } - return readFamilies(parser); - } - - private static FontListParser.Config readFamilies(XmlPullParser parser) - throws XmlPullParserException, IOException { - FontListParser.Config config = new FontListParser.Config(); - parser.require(XmlPullParser.START_TAG, null, "font-family"); - String tag = parser.getName(); - if (tag.equals("font-family")) { - config.families.add(readFamily(parser)); - } else { - skip(parser); - } - return config; - } - - private static FontListParser.Family readFamily(XmlPullParser parser) - throws XmlPullParserException, IOException { - String name = parser.getAttributeValue(null, "name"); - String lang = parser.getAttributeValue(null, "lang"); - String variant = parser.getAttributeValue(null, "variant"); - List<FontListParser.Font> fonts = new ArrayList<>(); - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) continue; - String tag = parser.getName(); - if (tag.equals("font")) { - fonts.add(readFont(parser)); - } else { - skip(parser); - } - } - return new FontListParser.Family(name, fonts, lang, variant); - } - - /** Matches leading and trailing XML whitespace. */ - private static final Pattern FILENAME_WHITESPACE_PATTERN = - Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); - - private static FontListParser.Font readFont(XmlPullParser parser) - throws XmlPullParserException, IOException { - - List<FontListParser.Axis> axes = new ArrayList<>(); - - String weightStr = parser.getAttributeValue(ANDROID_NAMESPACE, "fontWeight"); - int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); - - boolean isItalic = "italic".equals( - parser.getAttributeValue(ANDROID_NAMESPACE, "fontStyle")); - - String filename = parser.getAttributeValue(ANDROID_NAMESPACE, "font"); - String fullFilename = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); - return new FontListParser.Font(fullFilename, 0, axes, weight, isItalic); - } - - private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { - int depth = 1; - while (depth > 0) { - switch (parser.next()) { - case XmlPullParser.START_TAG: - depth++; - break; - case XmlPullParser.END_TAG: - depth--; - break; - } - } - } -} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index eebe538ccab8..0a349e91ad1e 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -18,6 +18,7 @@ package android.graphics; import android.annotation.NonNull; import android.content.res.AssetManager; +import android.text.FontConfig; import android.util.Log; import android.util.LongSparseArray; import android.util.LruCache; @@ -319,25 +320,25 @@ public class Typeface { mStyle = nativeGetStyle(ni); } - private static FontFamily makeFamilyFromParsed(FontListParser.Family family, + private static FontFamily makeFamilyFromParsed(FontConfig.Family family, Map<String, ByteBuffer> bufferForPath) { - FontFamily fontFamily = new FontFamily(family.lang, family.variant); - for (FontListParser.Font font : family.fonts) { - ByteBuffer fontBuffer = bufferForPath.get(font.fontName); + FontFamily fontFamily = new FontFamily(family.getLanguage(), family.getVariant()); + for (FontConfig.Font font : family.getFonts()) { + ByteBuffer fontBuffer = bufferForPath.get(font.getFontName()); if (fontBuffer == null) { - try (FileInputStream file = new FileInputStream(font.fontName)) { + try (FileInputStream file = new FileInputStream(font.getFontName())) { FileChannel fileChannel = file.getChannel(); long fontSize = fileChannel.size(); fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); - bufferForPath.put(font.fontName, fontBuffer); + bufferForPath.put(font.getFontName(), fontBuffer); } catch (IOException e) { - Log.e(TAG, "Error mapping font file " + font.fontName); + Log.e(TAG, "Error mapping font file " + font.getFontName()); continue; } } - if (!fontFamily.addFontWeightStyle(fontBuffer, font.ttcIndex, font.axes, - font.weight, font.isItalic)) { - Log.e(TAG, "Error creating font " + font.fontName + "#" + font.ttcIndex); + if (!fontFamily.addFontWeightStyle(fontBuffer, font.getTtcIndex(), font.getAxes(), + font.getWeight(), font.isItalic())) { + Log.e(TAG, "Error creating font " + font.getFontName() + "#" + font.getTtcIndex()); } } return fontFamily; @@ -354,16 +355,16 @@ public class Typeface { File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); try { FileInputStream fontsIn = new FileInputStream(configFilename); - FontListParser.Config fontConfig = FontListParser.parse(fontsIn); + FontConfig fontConfig = FontListParser.parse(fontsIn); Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>(); List<FontFamily> familyList = new ArrayList<FontFamily>(); // Note that the default typeface is always present in the fallback list; // this is an enhancement from pre-Minikin behavior. - for (int i = 0; i < fontConfig.families.size(); i++) { - FontListParser.Family f = fontConfig.families.get(i); - if (i == 0 || f.name == null) { + for (int i = 0; i < fontConfig.getFamilies().size(); i++) { + FontConfig.Family f = fontConfig.getFamilies().get(i); + if (i == 0 || f.getName() == null) { familyList.add(makeFamilyFromParsed(f, bufferForPath)); } } @@ -371,10 +372,10 @@ public class Typeface { setDefault(Typeface.createFromFamilies(sFallbackFonts)); Map<String, Typeface> systemFonts = new HashMap<String, Typeface>(); - for (int i = 0; i < fontConfig.families.size(); i++) { + for (int i = 0; i < fontConfig.getFamilies().size(); i++) { Typeface typeface; - FontListParser.Family f = fontConfig.families.get(i); - if (f.name != null) { + FontConfig.Family f = fontConfig.getFamilies().get(i); + if (f.getName() != null) { if (i == 0) { // The first entry is the default typeface; no sense in // duplicating the corresponding FontFamily. @@ -384,17 +385,17 @@ public class Typeface { FontFamily[] families = { fontFamily }; typeface = Typeface.createFromFamiliesWithDefault(families); } - systemFonts.put(f.name, typeface); + systemFonts.put(f.getName(), typeface); } } - for (FontListParser.Alias alias : fontConfig.aliases) { - Typeface base = systemFonts.get(alias.toName); + for (FontConfig.Alias alias : fontConfig.getAliases()) { + Typeface base = systemFonts.get(alias.getToName()); Typeface newFace = base; - int weight = alias.weight; + int weight = alias.getWeight(); if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } - systemFonts.put(alias.name, newFace); + systemFonts.put(alias.getName(), newFace); } sSystemFontMap = systemFonts; diff --git a/graphics/tests/graphicstests/src/android/graphics/VariationParserTest.java b/graphics/tests/graphicstests/src/android/graphics/VariationParserTest.java index d046c1103efc..2b4e6c27f193 100644 --- a/graphics/tests/graphicstests/src/android/graphics/VariationParserTest.java +++ b/graphics/tests/graphicstests/src/android/graphics/VariationParserTest.java @@ -17,50 +17,53 @@ package android.graphics; import android.test.suitebuilder.annotation.SmallTest; +import android.text.FontConfig; import junit.framework.TestCase; +import java.util.List; + public class VariationParserTest extends TestCase { @SmallTest public void testParseFontVariationSetting() { int tag = FontListParser.makeTag('w', 'd', 't', 'h'); - FontListParser.Axis[] axis = FontListParser.parseFontVariationSettings("'wdth' 1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + FontConfig.Axis[] axis = FontListParser.parseFontVariationSettings("'wdth' 1"); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("\"wdth\" 100"); - assertEquals(tag, axis[0].tag); - assertEquals(100.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(100.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings(" 'wdth' 100"); - assertEquals(tag, axis[0].tag); - assertEquals(100.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(100.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("\t'wdth' 0.5"); - assertEquals(tag, axis[0].tag); - assertEquals(0.5f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(0.5f, axis[0].getStyleValue()); tag = FontListParser.makeTag('A', 'X', ' ', ' '); axis = FontListParser.parseFontVariationSettings("'AX ' 1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("'AX '\t1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("'AX '\n1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("'AX '\r1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); axis = FontListParser.parseFontVariationSettings("'AX '\r\t\n 1"); - assertEquals(tag, axis[0].tag); - assertEquals(1.0f, axis[0].styleValue); + assertEquals(tag, axis[0].getTag()); + assertEquals(1.0f, axis[0].getStyleValue()); // Test for invalid input axis = FontListParser.parseFontVariationSettings(""); @@ -87,26 +90,26 @@ public class VariationParserTest extends TestCase { @SmallTest public void testParseFontVariationStyleSettings() { - FontListParser.Axis[] axis = + FontConfig.Axis[] axis = FontListParser.parseFontVariationSettings("'wdth' 10,'AX '\r1"); int tag1 = FontListParser.makeTag('w', 'd', 't', 'h'); int tag2 = FontListParser.makeTag('A', 'X', ' ', ' '); - assertEquals(tag1, axis[0].tag); - assertEquals(10.0f, axis[0].styleValue); - assertEquals(tag2, axis[1].tag); - assertEquals(1.0f, axis[1].styleValue); + assertEquals(tag1, axis[0].getTag()); + assertEquals(10.0f, axis[0].getStyleValue()); + assertEquals(tag2, axis[1].getTag()); + assertEquals(1.0f, axis[1].getStyleValue()); // Test only spacers are allowed before tag axis = FontListParser.parseFontVariationSettings(" 'wdth' 10,ab'wdth' 1"); tag1 = FontListParser.makeTag('w', 'd', 't', 'h'); - assertEquals(tag1, axis[0].tag); - assertEquals(10.0f, axis[0].styleValue); + assertEquals(tag1, axis[0].getTag()); + assertEquals(10.0f, axis[0].getStyleValue()); assertEquals(1, axis.length); } @SmallTest public void testInvalidTagCharacters() { - FontListParser.Axis[] axis = + FontConfig.Axis[] axis = FontListParser.parseFontVariationSettings("'\u0000\u0000\u0000\u0000' 10"); assertEquals(0, axis.length); axis = FontListParser.parseFontVariationSettings("'\u3042\u3044\u3046\u3048' 10"); diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp index a53a55a06964..1d8b021274fe 100644 --- a/libs/hwui/FrameBuilder.cpp +++ b/libs/hwui/FrameBuilder.cpp @@ -180,16 +180,18 @@ void FrameBuilder::deferRenderNodeScene(const std::vector< sp<RenderNode> >& nod } } - if (!backdrop.isEmpty()) { - // content node translation to catch up with backdrop - float dx = contentDrawBounds.left - backdrop.left; - float dy = contentDrawBounds.top - backdrop.top; - - Rect contentLocalClip = backdrop; - contentLocalClip.translate(dx, dy); - deferRenderNode(-dx, -dy, contentLocalClip, *nodes[1]); - } else { - deferRenderNode(*nodes[1]); + if (!nodes[1]->nothingToDraw()) { + if (!backdrop.isEmpty()) { + // content node translation to catch up with backdrop + float dx = contentDrawBounds.left - backdrop.left; + float dy = contentDrawBounds.top - backdrop.top; + + Rect contentLocalClip = backdrop; + contentLocalClip.translate(dx, dy); + deferRenderNode(-dx, -dy, contentLocalClip, *nodes[1]); + } else { + deferRenderNode(*nodes[1]); + } } // remaining overlay nodes, simply defer diff --git a/libs/hwui/tests/common/LeakChecker.cpp b/libs/hwui/tests/common/LeakChecker.cpp index d935382cc9a4..8a0b64b2f1cb 100644 --- a/libs/hwui/tests/common/LeakChecker.cpp +++ b/libs/hwui/tests/common/LeakChecker.cpp @@ -67,6 +67,12 @@ static void logUnreachable(initializer_list<UnreachableMemoryInfo> infolist) { } void LeakChecker::checkForLeaks() { + // TODO: Re-enable, disabled to workaround b/34586922 + if ((true)) { + cout << "checkForLeaks disabled, see b/34586922" << endl; + return; + } + // TODO: Until we can shutdown the RT thread we need to do this in // two passes as GetUnreachableMemory has limited insight into // thread-local caches so some leaks will not be properly tagged as leaks diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 275ce166728b..79daa3f9ffeb 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -193,10 +193,9 @@ SkColor TestUtils::getColor(const sk_sp<SkSurface>& surface, int x, int y) { } SkRect TestUtils::getClipBounds(const SkCanvas* canvas) { - SkClipStack::BoundsType boundType; - SkRect clipBounds; - canvas->getClipStack()->getBounds(&clipBounds, &boundType); - return clipBounds; + SkIRect bounds; + (void)canvas->getClipDeviceBounds(&bounds); + return SkRect::Make(bounds); } SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) { diff --git a/libs/hwui/tests/unit/FrameBuilderTests.cpp b/libs/hwui/tests/unit/FrameBuilderTests.cpp index 6f3ed9cf9e2f..5a2791cdfd82 100644 --- a/libs/hwui/tests/unit/FrameBuilderTests.cpp +++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp @@ -311,13 +311,32 @@ RENDERTHREAD_OPENGL_PIPELINE_TEST(FrameBuilder, deferRenderNodeScene) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); } - FrameBuilder frameBuilder(SkRect::MakeWH(800, 600), 800, 600, - sLightGeometry, Caches::getInstance()); - frameBuilder.deferRenderNodeScene(nodes, contentDrawBounds); + { + FrameBuilder frameBuilder(SkRect::MakeWH(800, 600), 800, 600, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNodeScene(nodes, contentDrawBounds); - DeferRenderNodeSceneTestRenderer renderer; - frameBuilder.replayBakedOps<TestDispatcher>(renderer); - EXPECT_EQ(4, renderer.getIndex()); + DeferRenderNodeSceneTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(4, renderer.getIndex()); + } + + for (auto& node : nodes) { + EXPECT_FALSE(node->nothingToDraw()); + node->setStagingDisplayList(nullptr, nullptr); + node->destroyHardwareResources(nullptr); + EXPECT_TRUE(node->nothingToDraw()); + } + + { + // Validate no crashes if any nodes are missing DisplayLists + FrameBuilder frameBuilder(SkRect::MakeWH(800, 600), 800, 600, + sLightGeometry, Caches::getInstance()); + frameBuilder.deferRenderNodeScene(nodes, contentDrawBounds); + + FailRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + } } RENDERTHREAD_OPENGL_PIPELINE_TEST(FrameBuilder, empty_noFbo0) { diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index f5ff05849793..2f1eae3ec10e 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -865,10 +865,8 @@ RENDERTHREAD_TEST(RenderNodeDrawable, colorOp_unbounded) { void onDrawPaint(const SkPaint&) { switch (mDrawCounter++) { case 0: - // While this mirrors FrameBuilder::colorOp_unbounded, this value is different - // because there is no root (root is clipped in SkiaPipeline::renderFrame). - // SkiaPipeline.clipped and clip_replace verify the root clip. - EXPECT_TRUE(TestUtils::getClipBounds(this).isEmpty()); + EXPECT_EQ(SkRect::MakeWH(CANVAS_WIDTH, CANVAS_HEIGHT), + TestUtils::getClipBounds(this)); break; case 1: EXPECT_EQ(SkRect::MakeWH(10, 10), TestUtils::getClipBounds(this)); diff --git a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp index 92d9d3d0d5fe..95c6ed6059b0 100644 --- a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp +++ b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp @@ -87,14 +87,7 @@ TEST(RenderNodeDrawable, renderPropRevealClip) { testProperty([](RenderProperties& properties) { properties.mutableRevealClip().set(true, 50, 50, 25); }, [](const SkCanvas& canvas) { - SkClipStack::Iter it(*canvas.getClipStack(), SkClipStack::Iter::kBottom_IterStart); - const SkClipStack::Element *top = it.next(); - ASSERT_NE(nullptr, top); - SkPath clip; - top->asPath(&clip); - SkRect rect; - EXPECT_TRUE(clip.isOval(&rect)); - EXPECT_EQ(SkRect::MakeLTRB(25, 25, 75, 75), rect); + EXPECT_EQ(SkRect::MakeLTRB(25, 25, 75, 75), TestUtils::getClipBounds(&canvas)); }); } @@ -103,14 +96,7 @@ TEST(RenderNodeDrawable, renderPropOutlineClip) { properties.mutableOutline().setShouldClip(true); properties.mutableOutline().setRoundRect(10, 20, 30, 40, 5.0f, 0.5f); }, [](const SkCanvas& canvas) { - SkClipStack::Iter it(*canvas.getClipStack(), SkClipStack::Iter::kBottom_IterStart); - const SkClipStack::Element *top = it.next(); - ASSERT_NE(nullptr, top); - SkPath clip; - top->asPath(&clip); - SkRRect rrect; - EXPECT_TRUE(clip.isRRect(&rrect)); - EXPECT_EQ(SkRRect::MakeRectXY(SkRect::MakeLTRB(10, 20, 30, 40), 5.0f, 5.0f), rrect); + EXPECT_EQ(SkRect::MakeLTRB(10, 20, 30, 40), TestUtils::getClipBounds(&canvas)); }); } diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java index ce23176dd7db..aac9727614ea 100644 --- a/location/java/android/location/GnssMeasurement.java +++ b/location/java/android/location/GnssMeasurement.java @@ -660,8 +660,14 @@ public final class GnssMeasurement implements Parcelable { /** * Gets the carrier frequency of the tracked signal. * - * <p>For example it can be the GPS L1 = 1.57542e9 Hz, or L2, L5, varying GLO channels, etc. If - * the field is not set, it is the primary common use frequency, e.g. L1 for GPS. + * <p>For example it can be the GPS central frequency for L1 = 1575.45 MHz, or L2 = 1227.60 MHz, + * L5 = 1176.45 MHz, varying GLO channels, etc. If the field is not set, it is the primary + * common use central frequency, e.g. L1 = 1575.45 MHz for GPS. + * + * For an L1, L5 receiver tracking a satellite on L1 and L5 at the same time, two raw + * measurement objects will be reported for this same satellite, in one of the measurement + * objects, all the values related to L1 will be filled, and in the other all of the values + * related to L5 will be filled. * * <p>The value is only available if {@link #hasCarrierFrequencyHz()} is {@code true}. */ @@ -882,7 +888,7 @@ public final class GnssMeasurement implements Parcelable { } /** - * Returns {@code true} if {@link #getAutomaticGainControlLevelInDb()} is available, + * Returns {@code true} if {@link #getAutomaticGainControlLevelInDb()} is available, * {@code false} otherwise. */ public boolean hasAutomaticGainControlLevelInDb() { @@ -892,11 +898,12 @@ public final class GnssMeasurement implements Parcelable { /** * Gets the Automatic Gain Control level in dB. * - * <p> AGC acts as a variable gain amplifier adjusting the power of the incoming signal to - * minimize the quantization losses. The AGC level may be used to indicate potential - * interference. When AGC is at a nominal level, this value must be set as 0. Higher gain - * (and/or lower input power) shall be output as a positive number. Hence in cases of strong - * jamming, in the band of this signal, this value will go more negative. + * <p> AGC acts as a variable gain amplifier adjusting the power of the incoming signal. The AGC + * level may be used to indicate potential interference. When AGC is at a nominal level, this + * value must be set as 0. Higher gain (and/or lower input power) shall be output as a positive + * number. Hence in cases of strong jamming, in the band of this signal, this value will go more + * negative. + * * <p>Note: Different hardware designs (e.g. antenna, pre-amplification, or other RF HW * components) may also affect the typical output of of this value on any given hardware design * in an open sky test - the important aspect of this output is that changes in this value are diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java index 78dbc71268ac..e90a174eb9be 100644 --- a/location/java/android/location/GnssStatus.java +++ b/location/java/android/location/GnssStatus.java @@ -231,8 +231,13 @@ public final class GnssStatus { /** * Gets the carrier frequency of the signal tracked. * - * For example it can be the GPS L1 = 1.57542e9 Hz, or L2, L5, varying GLO channels, etc. If - * the field is not set, it is the primary common use frequency, e.g. L1 for GPS. + * <p>For example it can be the GPS central frequency for L1 = 1575.45 MHz, or L2 = 1227.60 MHz, + * L5 = 1176.45 MHz, varying GLO channels, etc. If the field is not set, it is the primary + * common use central frequency, e.g. L1 = 1575.45 MHz for GPS. + * + * For an L1, L5 receiver tracking a satellite on L1 and L5 at the same time, two measurements + * will be reported for this same satellite, in one all the values related to L1 will be filled, + * and in the other all of the values related to L5 will be filled. * * <p>The value is only available if {@link #hasCarrierFrequency(int satIndex)} is {@code true}. */ diff --git a/native/android/Android.mk b/native/android/Android.mk index da4e4bac6dbc..355f52e1b7e4 100644 --- a/native/android/Android.mk +++ b/native/android/Android.mk @@ -9,6 +9,7 @@ LOCAL_SRC_FILES:= \ asset_manager.cpp \ choreographer.cpp \ configuration.cpp \ + hardware_buffer.cpp \ input.cpp \ looper.cpp \ native_activity.cpp \ diff --git a/native/android/hardware_buffer.cpp b/native/android/hardware_buffer.cpp new file mode 100644 index 000000000000..6a10cb587be2 --- /dev/null +++ b/native/android/hardware_buffer.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "AHardwareBuffer" + +#include <android/hardware_buffer_jni.h> + +#include <errno.h> +#include <sys/socket.h> + +#include <memory> + +#include <android_runtime/android_hardware_HardwareBuffer.h> +#include <binder/Binder.h> +#include <binder/Parcel.h> +#include <cutils/native_handle.h> +#include <binder/IServiceManager.h> +#include <gui/ISurfaceComposer.h> +#include <gui/IGraphicBufferAlloc.h> +#include <ui/GraphicBuffer.h> +#include <utils/Flattenable.h> +#include <utils/Log.h> + +static constexpr int kDataBufferSize = 64 * sizeof(int); // 64 ints + +using namespace android; + +static inline const GraphicBuffer* AHardwareBuffer_to_GraphicBuffer( + const AHardwareBuffer* buffer) { + return reinterpret_cast<const GraphicBuffer*>(buffer); +} + +static inline GraphicBuffer* AHardwareBuffer_to_GraphicBuffer( + AHardwareBuffer* buffer) { + return reinterpret_cast<GraphicBuffer*>(buffer); +} + +static inline AHardwareBuffer* GraphicBuffer_to_AHardwareBuffer( + GraphicBuffer* buffer) { + return reinterpret_cast<AHardwareBuffer*>(buffer); +} + +// ---------------------------------------------------------------------------- +// Public functions +// ---------------------------------------------------------------------------- + +int AHardwareBuffer_allocate(const AHardwareBuffer_Desc* desc, + AHardwareBuffer** outBuffer) { + if (!outBuffer || !desc) return BAD_VALUE; + + // The holder is used to destroy the buffer if an error occurs. + sp<IServiceManager> sm = defaultServiceManager(); + if (sm == nullptr) { + ALOGE("Unable to connect to ServiceManager"); + return PERMISSION_DENIED; + } + + // Get the SurfaceFlingerService. + sp<ISurfaceComposer> composer = interface_cast<ISurfaceComposer>( + sm->getService(String16("SurfaceFlinger"))); + if (composer == nullptr) { + ALOGE("Unable to connect to surface composer"); + return PERMISSION_DENIED; + } + // Get an IGraphicBufferAlloc to create the buffer. + sp<IGraphicBufferAlloc> allocator = composer->createGraphicBufferAlloc(); + if (allocator == nullptr) { + ALOGE("Unable to obtain a buffer allocator"); + return PERMISSION_DENIED; + } + + int format = android_hardware_HardwareBuffer_convertToPixelFormat( + desc->format); + if (format == 0) { + ALOGE("Invalid pixel format"); + return BAD_VALUE; + } + + status_t err; + uint32_t usage = android_hardware_HardwareBuffer_convertToGrallocUsageBits( + desc->usage0, desc->usage1); + sp<GraphicBuffer> gbuffer = allocator->createGraphicBuffer(desc->width, + desc->height, format, desc->layers, usage, &err); + if (err != NO_ERROR) { + return err; + } + + *outBuffer = GraphicBuffer_to_AHardwareBuffer(gbuffer.get()); + // Ensure the buffer doesn't get destroyed with the sp<> goes away. + AHardwareBuffer_acquire(*outBuffer); + return NO_ERROR; +} + +void AHardwareBuffer_acquire(AHardwareBuffer* buffer) { + AHardwareBuffer_to_GraphicBuffer(buffer)->incStrong( + (void*)AHardwareBuffer_acquire); +} + +void AHardwareBuffer_release(AHardwareBuffer* buffer) { + AHardwareBuffer_to_GraphicBuffer(buffer)->decStrong( + (void*)AHardwareBuffer_acquire); +} + +void AHardwareBuffer_describe(const AHardwareBuffer* buffer, + AHardwareBuffer_Desc* outDesc) { + if (!buffer || !outDesc) return; + + const GraphicBuffer* gbuffer = AHardwareBuffer_to_GraphicBuffer(buffer); + + outDesc->width = gbuffer->getWidth(); + outDesc->height = gbuffer->getHeight(); + outDesc->layers = gbuffer->getLayerCount(); + outDesc->usage0 = + android_hardware_HardwareBuffer_convertFromGrallocUsageBits( + gbuffer->getUsage()); + outDesc->usage1 = 0; + outDesc->format = android_hardware_HardwareBuffer_convertFromPixelFormat( + static_cast<uint32_t>(gbuffer->getPixelFormat())); +} + +int AHardwareBuffer_lock(AHardwareBuffer* buffer, uint64_t usage0, + int32_t fence, const ARect* rect, void** outVirtualAddress) { + if (!buffer) return BAD_VALUE; + + if (usage0 & ~(AHARDWAREBUFFER_USAGE0_CPU_READ_OFTEN | + AHARDWAREBUFFER_USAGE0_CPU_WRITE_OFTEN)) { + ALOGE("Invalid usage flags passed to AHardwareBuffer_lock; only " + " AHARDWAREBUFFER_USAGE0_CPU_* flags are allowed"); + return BAD_VALUE; + } + + uint32_t usage = android_hardware_HardwareBuffer_convertToGrallocUsageBits( + usage0, 0); + GraphicBuffer* gBuffer = AHardwareBuffer_to_GraphicBuffer(buffer); + if (!rect) { + return gBuffer->lockAsync(usage, outVirtualAddress, fence); + } else { + Rect bounds(rect->left, rect->top, rect->right, rect->bottom); + return gBuffer->lockAsync(usage, bounds, outVirtualAddress, fence); + } +} + +int AHardwareBuffer_unlock(AHardwareBuffer* buffer, int32_t* fence) { + if (!buffer) return BAD_VALUE; + + GraphicBuffer* gBuffer = AHardwareBuffer_to_GraphicBuffer(buffer); + return gBuffer->unlockAsync(fence); +} + +int AHardwareBuffer_sendHandleToUnixSocket(const AHardwareBuffer* buffer, + int socketFd) { + if (!buffer) return BAD_VALUE; + const GraphicBuffer* gBuffer = AHardwareBuffer_to_GraphicBuffer(buffer); + + size_t flattenedSize = gBuffer->getFlattenedSize(); + size_t fdCount = gBuffer->getFdCount(); + + std::unique_ptr<uint8_t[]> data(new uint8_t[flattenedSize]); + std::unique_ptr<int[]> fds(new int[fdCount]); + + // Make copies of needed items since flatten modifies them, and we don't + // want to send anything if there's an error during flatten. + size_t flattenedSizeCopy = flattenedSize; + size_t fdCountCopy = fdCount; + void* dataStart = data.get(); + int* fdsStart = fds.get(); + status_t err = gBuffer->flatten(dataStart, flattenedSizeCopy, fdsStart, + fdCountCopy); + if (err != NO_ERROR) { + return err; + } + + struct iovec iov[1]; + iov[0].iov_base = data.get(); + iov[0].iov_len = flattenedSize; + + char buf[CMSG_SPACE(kDataBufferSize)]; + struct msghdr msg = { + .msg_control = buf, + .msg_controllen = sizeof(buf), + .msg_iov = &iov[0], + .msg_iovlen = 1, + }; + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int) * fdCount); + int* fdData = reinterpret_cast<int*>(CMSG_DATA(cmsg)); + memcpy(fdData, fds.get(), sizeof(int) * fdCount); + msg.msg_controllen = cmsg->cmsg_len; + + int result = sendmsg(socketFd, &msg, 0); + if (result <= 0) { + ALOGE("Error writing AHardwareBuffer to socket: error %#x (%s)", + result, strerror(errno)); + return result; + } + return NO_ERROR; +} + +int AHardwareBuffer_recvHandleFromUnixSocket(int socketFd, + AHardwareBuffer** outBuffer) { + if (!outBuffer) return BAD_VALUE; + + char dataBuf[CMSG_SPACE(kDataBufferSize)]; + char fdBuf[CMSG_SPACE(kDataBufferSize)]; + struct iovec iov[1]; + iov[0].iov_base = dataBuf; + iov[0].iov_len = sizeof(dataBuf); + + struct msghdr msg = { + .msg_control = fdBuf, + .msg_controllen = sizeof(fdBuf), + .msg_iov = &iov[0], + .msg_iovlen = 1, + }; + + int result = recvmsg(socketFd, &msg, 0); + if (result <= 0) { + ALOGE("Error reading AHardwareBuffer from socket: error %#x (%s)", + result, strerror(errno)); + return result; + } + + if (msg.msg_iovlen != 1) { + ALOGE("Error reading AHardwareBuffer from socket: bad data length"); + return INVALID_OPERATION; + } + + if (msg.msg_controllen % sizeof(int) != 0) { + ALOGE("Error reading AHardwareBuffer from socket: bad fd length"); + return INVALID_OPERATION; + } + + size_t dataLen = msg.msg_iov[0].iov_len; + const void* data = static_cast<const void*>(msg.msg_iov[0].iov_base); + if (!data) { + ALOGE("Error reading AHardwareBuffer from socket: no buffer data"); + return INVALID_OPERATION; + } + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) { + ALOGE("Error reading AHardwareBuffer from socket: no fd header"); + return INVALID_OPERATION; + } + + size_t fdCount = msg.msg_controllen >> 2; + const int* fdData = reinterpret_cast<const int*>(CMSG_DATA(cmsg)); + if (!fdData) { + ALOGE("Error reading AHardwareBuffer from socket: no fd data"); + return INVALID_OPERATION; + } + + GraphicBuffer* gBuffer = new GraphicBuffer(); + status_t err = gBuffer->unflatten(data, dataLen, fdData, fdCount); + if (err != NO_ERROR) { + return err; + } + *outBuffer = GraphicBuffer_to_AHardwareBuffer(gBuffer); + // Ensure the buffer has a positive ref-count. + AHardwareBuffer_acquire(*outBuffer); + + return NO_ERROR; +} + +const struct native_handle* AHardwareBuffer_getNativeHandle( + const AHardwareBuffer* buffer) { + if (!buffer) return nullptr; + const GraphicBuffer* gbuffer = AHardwareBuffer_to_GraphicBuffer(buffer); + return gbuffer->handle; +} + +// ---------------------------------------------------------------------------- +// JNI functions +// ---------------------------------------------------------------------------- + +AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(JNIEnv* env, + jobject hardwareBufferObj) { + return android_hardware_HardwareBuffer_getNativeHardwareBuffer(env, + hardwareBufferObj); +} + +jobject AHardwareBuffer_toHardwareBuffer(JNIEnv* env, + AHardwareBuffer* hardwareBuffer) { + return android_hardware_HardwareBuffer_createFromAHardwareBuffer(env, + hardwareBuffer); +} diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 5758a3ce2a25..f9e8fda81c09 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -139,6 +139,17 @@ LIBANDROID { ANativeActivity_setWindowFlags; ANativeActivity_setWindowFormat; ANativeActivity_showSoftInput; + AHardwareBuffer_acquire; # introduced=26 + AHardwareBuffer_allocate; # introduced=26 + AHardwareBuffer_describe; # introduced=26 + AHardwareBuffer_fromHardwareBuffer; # introduced=26 + AHardwareBuffer_getNativeHandle; # introduced=26 + AHardwareBuffer_lock; # introduced=26 + AHardwareBuffer_recvHandleFromUnixSocket; # introduced=26 + AHardwareBuffer_release; # introduced=26 + AHardwareBuffer_sendHandleToUnixSocket; # introduced=26 + AHardwareBuffer_toHardwareBuffer; # introduced=26 + AHardwareBuffer_unlock; # introduced=26 ANativeWindow_acquire; ANativeWindow_fromSurface; ANativeWindow_fromSurfaceTexture; # introduced-arm=13 introduced-mips=13 introduced-x86=13 diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java index d34dd896bd2d..d4623d6f46f6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java @@ -22,13 +22,22 @@ import android.content.res.Resources; public class InterestingConfigChanges { private final Configuration mLastConfiguration = new Configuration(); + private final int mFlags; private int mLastDensity; + public InterestingConfigChanges() { + this(0); + } + + public InterestingConfigChanges(int extraFlags) { + mFlags = extraFlags | ActivityInfo.CONFIG_LOCALE + | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_SCREEN_LAYOUT; + } + public boolean applyNewConfig(Resources res) { int configChanges = mLastConfiguration.updateFrom(res.getConfiguration()); boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi; - if (densityChanged || (configChanges&(ActivityInfo.CONFIG_LOCALE - |ActivityInfo.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) { + if (densityChanged || (configChanges & (mFlags)) != 0) { mLastDensity = res.getDisplayMetrics().densityDpi; return true; } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java new file mode 100644 index 000000000000..19ce3d008643 --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -0,0 +1,1655 @@ +/* + * Copyright (C) 2007 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.providers.settings; + +import android.annotation.NonNull; +import android.os.UserHandle; +import android.provider.Settings; +import android.providers.settings.GlobalSettingsProto; +import android.providers.settings.SecureSettingsProto; +import android.providers.settings.SettingProto; +import android.providers.settings.SettingsServiceDumpProto; +import android.providers.settings.SystemSettingsProto; +import android.providers.settings.UserSettingsProto; +import android.util.SparseBooleanArray; +import android.util.proto.ProtoOutputStream; + +/** @hide */ +class SettingsProtoDumpUtil { + private SettingsProtoDumpUtil() {} + + static void dumpProtoLocked(SettingsProvider.SettingsRegistry settingsRegistry, + ProtoOutputStream proto) { + // Global settings + SettingsState globalSettings = settingsRegistry.getSettingsLocked( + SettingsProvider.SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM); + long globalSettingsToken = proto.start(SettingsServiceDumpProto.GLOBAL_SETTINGS); + dumpProtoGlobalSettingsLocked(globalSettings, proto); + proto.end(globalSettingsToken); + + // Per-user settings + SparseBooleanArray users = settingsRegistry.getKnownUsersLocked(); + final int userCount = users.size(); + for (int i = 0; i < userCount; i++) { + long userSettingsToken = proto.start(SettingsServiceDumpProto.USER_SETTINGS); + dumpProtoUserSettingsLocked( + settingsRegistry, UserHandle.of(users.keyAt(i)), proto); + proto.end(userSettingsToken); + } + } + + /** + * Dump all settings of a user as a proto buf. + * + * @param settingsRegistry + * @param user The user the settings should be dumped for + * @param proto The proto buf stream to dump to + */ + private static void dumpProtoUserSettingsLocked( + SettingsProvider.SettingsRegistry settingsRegistry, + @NonNull UserHandle user, + @NonNull ProtoOutputStream proto) { + proto.write(UserSettingsProto.USER_ID, user.getIdentifier()); + + SettingsState secureSettings = settingsRegistry.getSettingsLocked( + SettingsProvider.SETTINGS_TYPE_SECURE, user.getIdentifier()); + long secureSettingsToken = proto.start(UserSettingsProto.SECURE_SETTINGS); + dumpProtoSecureSettingsLocked(secureSettings, proto); + proto.end(secureSettingsToken); + + SettingsState systemSettings = settingsRegistry.getSettingsLocked( + SettingsProvider.SETTINGS_TYPE_SYSTEM, user.getIdentifier()); + long systemSettingsToken = proto.start(UserSettingsProto.SYSTEM_SETTINGS); + dumpProtoSystemSettingsLocked(systemSettings, proto); + proto.end(systemSettingsToken); + } + + private static void dumpProtoGlobalSettingsLocked( + @NonNull SettingsState s, @NonNull ProtoOutputStream p) { + dumpSetting(s, p, + Settings.Global.ADD_USERS_WHEN_LOCKED, + GlobalSettingsProto.ADD_USERS_WHEN_LOCKED); + dumpSetting(s, p, + Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, + GlobalSettingsProto.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED); + dumpSetting(s, p, + Settings.Global.AIRPLANE_MODE_ON, + GlobalSettingsProto.AIRPLANE_MODE_ON); + dumpSetting(s, p, + Settings.Global.THEATER_MODE_ON, + GlobalSettingsProto.THEATER_MODE_ON); + dumpSetting(s, p, + Settings.Global.RADIO_BLUETOOTH, + GlobalSettingsProto.RADIO_BLUETOOTH); + dumpSetting(s, p, + Settings.Global.RADIO_WIFI, + GlobalSettingsProto.RADIO_WIFI); + dumpSetting(s, p, + Settings.Global.RADIO_WIMAX, + GlobalSettingsProto.RADIO_WIMAX); + dumpSetting(s, p, + Settings.Global.RADIO_CELL, + GlobalSettingsProto.RADIO_CELL); + dumpSetting(s, p, + Settings.Global.RADIO_NFC, + GlobalSettingsProto.RADIO_NFC); + dumpSetting(s, p, + Settings.Global.AIRPLANE_MODE_RADIOS, + GlobalSettingsProto.AIRPLANE_MODE_RADIOS); + dumpSetting(s, p, + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, + GlobalSettingsProto.AIRPLANE_MODE_TOGGLEABLE_RADIOS); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_DISABLED_PROFILES, + GlobalSettingsProto.BLUETOOTH_DISABLED_PROFILES); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_INTEROPERABILITY_LIST, + GlobalSettingsProto.BLUETOOTH_INTEROPERABILITY_LIST); + dumpSetting(s, p, + Settings.Global.WIFI_SLEEP_POLICY, + GlobalSettingsProto.WIFI_SLEEP_POLICY); + dumpSetting(s, p, + Settings.Global.AUTO_TIME, + GlobalSettingsProto.AUTO_TIME); + dumpSetting(s, p, + Settings.Global.AUTO_TIME_ZONE, + GlobalSettingsProto.AUTO_TIME_ZONE); + dumpSetting(s, p, + Settings.Global.CAR_DOCK_SOUND, + GlobalSettingsProto.CAR_DOCK_SOUND); + dumpSetting(s, p, + Settings.Global.CAR_UNDOCK_SOUND, + GlobalSettingsProto.CAR_UNDOCK_SOUND); + dumpSetting(s, p, + Settings.Global.DESK_DOCK_SOUND, + GlobalSettingsProto.DESK_DOCK_SOUND); + dumpSetting(s, p, + Settings.Global.DESK_UNDOCK_SOUND, + GlobalSettingsProto.DESK_UNDOCK_SOUND); + dumpSetting(s, p, + Settings.Global.DOCK_SOUNDS_ENABLED, + GlobalSettingsProto.DOCK_SOUNDS_ENABLED); + dumpSetting(s, p, + Settings.Global.DOCK_SOUNDS_ENABLED_WHEN_ACCESSIBILITY, + GlobalSettingsProto.DOCK_SOUNDS_ENABLED_WHEN_ACCESSIBILITY); + dumpSetting(s, p, + Settings.Global.LOCK_SOUND, + GlobalSettingsProto.LOCK_SOUND); + dumpSetting(s, p, + Settings.Global.UNLOCK_SOUND, + GlobalSettingsProto.UNLOCK_SOUND); + dumpSetting(s, p, + Settings.Global.TRUSTED_SOUND, + GlobalSettingsProto.TRUSTED_SOUND); + dumpSetting(s, p, + Settings.Global.LOW_BATTERY_SOUND, + GlobalSettingsProto.LOW_BATTERY_SOUND); + dumpSetting(s, p, + Settings.Global.POWER_SOUNDS_ENABLED, + GlobalSettingsProto.POWER_SOUNDS_ENABLED); + dumpSetting(s, p, + Settings.Global.WIRELESS_CHARGING_STARTED_SOUND, + GlobalSettingsProto.WIRELESS_CHARGING_STARTED_SOUND); + dumpSetting(s, p, + Settings.Global.CHARGING_SOUNDS_ENABLED, + GlobalSettingsProto.CHARGING_SOUNDS_ENABLED); + dumpSetting(s, p, + Settings.Global.STAY_ON_WHILE_PLUGGED_IN, + GlobalSettingsProto.STAY_ON_WHILE_PLUGGED_IN); + dumpSetting(s, p, + Settings.Global.BUGREPORT_IN_POWER_MENU, + GlobalSettingsProto.BUGREPORT_IN_POWER_MENU); + dumpSetting(s, p, + Settings.Global.ADB_ENABLED, + GlobalSettingsProto.ADB_ENABLED); + dumpSetting(s, p, + Settings.Global.DEBUG_VIEW_ATTRIBUTES, + GlobalSettingsProto.DEBUG_VIEW_ATTRIBUTES); + dumpSetting(s, p, + Settings.Global.ASSISTED_GPS_ENABLED, + GlobalSettingsProto.ASSISTED_GPS_ENABLED); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_ON, + GlobalSettingsProto.BLUETOOTH_ON); + dumpSetting(s, p, + Settings.Global.CDMA_CELL_BROADCAST_SMS, + GlobalSettingsProto.CDMA_CELL_BROADCAST_SMS); + dumpSetting(s, p, + Settings.Global.CDMA_ROAMING_MODE, + GlobalSettingsProto.CDMA_ROAMING_MODE); + dumpSetting(s, p, + Settings.Global.CDMA_SUBSCRIPTION_MODE, + GlobalSettingsProto.CDMA_SUBSCRIPTION_MODE); + dumpSetting(s, p, + Settings.Global.DATA_ACTIVITY_TIMEOUT_MOBILE, + GlobalSettingsProto.DATA_ACTIVITY_TIMEOUT_MOBILE); + dumpSetting(s, p, + Settings.Global.DATA_ACTIVITY_TIMEOUT_WIFI, + GlobalSettingsProto.DATA_ACTIVITY_TIMEOUT_WIFI); + dumpSetting(s, p, + Settings.Global.DATA_ROAMING, + GlobalSettingsProto.DATA_ROAMING); + dumpSetting(s, p, + Settings.Global.MDC_INITIAL_MAX_RETRY, + GlobalSettingsProto.MDC_INITIAL_MAX_RETRY); + dumpSetting(s, p, + Settings.Global.FORCE_ALLOW_ON_EXTERNAL, + GlobalSettingsProto.FORCE_ALLOW_ON_EXTERNAL); + dumpSetting(s, p, + Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, + GlobalSettingsProto.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES); + dumpSetting(s, p, + Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, + GlobalSettingsProto.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT); + dumpSetting(s, p, + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, + GlobalSettingsProto.DEVELOPMENT_SETTINGS_ENABLED); + dumpSetting(s, p, + Settings.Global.DEVICE_PROVISIONED, + GlobalSettingsProto.DEVICE_PROVISIONED); + dumpSetting(s, p, + Settings.Global.DEVICE_PROVISIONING_MOBILE_DATA_ENABLED, + GlobalSettingsProto.DEVICE_PROVISIONING_MOBILE_DATA_ENABLED); + dumpSetting(s, p, + Settings.Global.DISPLAY_SIZE_FORCED, + GlobalSettingsProto.DISPLAY_SIZE_FORCED); + dumpSetting(s, p, + Settings.Global.DISPLAY_SCALING_FORCE, + GlobalSettingsProto.DISPLAY_SCALING_FORCE); + dumpSetting(s, p, + Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE, + GlobalSettingsProto.DOWNLOAD_MAX_BYTES_OVER_MOBILE); + dumpSetting(s, p, + Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE, + GlobalSettingsProto.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE); + dumpSetting(s, p, + Settings.Global.HDMI_CONTROL_ENABLED, + GlobalSettingsProto.HDMI_CONTROL_ENABLED); + dumpSetting(s, p, + Settings.Global.HDMI_SYSTEM_AUDIO_ENABLED, + GlobalSettingsProto.HDMI_SYSTEM_AUDIO_ENABLED); + dumpSetting(s, p, + Settings.Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, + GlobalSettingsProto.HDMI_CONTROL_AUTO_WAKEUP_ENABLED); + dumpSetting(s, p, + Settings.Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, + GlobalSettingsProto.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED); + dumpSetting(s, p, + Settings.Global.MHL_INPUT_SWITCHING_ENABLED, + GlobalSettingsProto.MHL_INPUT_SWITCHING_ENABLED); + dumpSetting(s, p, + Settings.Global.MHL_POWER_CHARGE_ENABLED, + GlobalSettingsProto.MHL_POWER_CHARGE_ENABLED); + dumpSetting(s, p, + Settings.Global.MOBILE_DATA, + GlobalSettingsProto.MOBILE_DATA); + dumpSetting(s, p, + Settings.Global.MOBILE_DATA_ALWAYS_ON, + GlobalSettingsProto.MOBILE_DATA_ALWAYS_ON); + dumpSetting(s, p, + Settings.Global.CONNECTIVITY_METRICS_BUFFER_SIZE, + GlobalSettingsProto.CONNECTIVITY_METRICS_BUFFER_SIZE); + dumpSetting(s, p, + Settings.Global.NETSTATS_ENABLED, + GlobalSettingsProto.NETSTATS_ENABLED); + dumpSetting(s, p, + Settings.Global.NETSTATS_POLL_INTERVAL, + GlobalSettingsProto.NETSTATS_POLL_INTERVAL); + dumpSetting(s, p, + Settings.Global.NETSTATS_TIME_CACHE_MAX_AGE, + GlobalSettingsProto.NETSTATS_TIME_CACHE_MAX_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_GLOBAL_ALERT_BYTES, + GlobalSettingsProto.NETSTATS_GLOBAL_ALERT_BYTES); + dumpSetting(s, p, + Settings.Global.NETSTATS_SAMPLE_ENABLED, + GlobalSettingsProto.NETSTATS_SAMPLE_ENABLED); + dumpSetting(s, p, + Settings.Global.NETSTATS_DEV_BUCKET_DURATION, + GlobalSettingsProto.NETSTATS_DEV_BUCKET_DURATION); + dumpSetting(s, p, + Settings.Global.NETSTATS_DEV_PERSIST_BYTES, + GlobalSettingsProto.NETSTATS_DEV_PERSIST_BYTES); + dumpSetting(s, p, + Settings.Global.NETSTATS_DEV_ROTATE_AGE, + GlobalSettingsProto.NETSTATS_DEV_ROTATE_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_DEV_DELETE_AGE, + GlobalSettingsProto.NETSTATS_DEV_DELETE_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_BUCKET_DURATION, + GlobalSettingsProto.NETSTATS_UID_BUCKET_DURATION); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_PERSIST_BYTES, + GlobalSettingsProto.NETSTATS_UID_PERSIST_BYTES); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_ROTATE_AGE, + GlobalSettingsProto.NETSTATS_UID_ROTATE_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_DELETE_AGE, + GlobalSettingsProto.NETSTATS_UID_DELETE_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_TAG_BUCKET_DURATION, + GlobalSettingsProto.NETSTATS_UID_TAG_BUCKET_DURATION); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_TAG_PERSIST_BYTES, + GlobalSettingsProto.NETSTATS_UID_TAG_PERSIST_BYTES); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_TAG_ROTATE_AGE, + GlobalSettingsProto.NETSTATS_UID_TAG_ROTATE_AGE); + dumpSetting(s, p, + Settings.Global.NETSTATS_UID_TAG_DELETE_AGE, + GlobalSettingsProto.NETSTATS_UID_TAG_DELETE_AGE); + dumpSetting(s, p, + Settings.Global.NETWORK_PREFERENCE, + GlobalSettingsProto.NETWORK_PREFERENCE); + dumpSetting(s, p, + Settings.Global.NETWORK_SCORER_APP, + GlobalSettingsProto.NETWORK_SCORER_APP); + dumpSetting(s, p, + Settings.Global.NITZ_UPDATE_DIFF, + GlobalSettingsProto.NITZ_UPDATE_DIFF); + dumpSetting(s, p, + Settings.Global.NITZ_UPDATE_SPACING, + GlobalSettingsProto.NITZ_UPDATE_SPACING); + dumpSetting(s, p, + Settings.Global.NTP_SERVER, + GlobalSettingsProto.NTP_SERVER); + dumpSetting(s, p, + Settings.Global.NTP_TIMEOUT, + GlobalSettingsProto.NTP_TIMEOUT); + dumpSetting(s, p, + Settings.Global.STORAGE_BENCHMARK_INTERVAL, + GlobalSettingsProto.STORAGE_BENCHMARK_INTERVAL); + dumpSetting(s, p, + Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, + GlobalSettingsProto.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS); + dumpSetting(s, p, + Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, + GlobalSettingsProto.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT); + dumpSetting(s, p, + Settings.Global.DNS_RESOLVER_MIN_SAMPLES, + GlobalSettingsProto.DNS_RESOLVER_MIN_SAMPLES); + dumpSetting(s, p, + Settings.Global.DNS_RESOLVER_MAX_SAMPLES, + GlobalSettingsProto.DNS_RESOLVER_MAX_SAMPLES); + dumpSetting(s, p, + Settings.Global.OTA_DISABLE_AUTOMATIC_UPDATE, + GlobalSettingsProto.OTA_DISABLE_AUTOMATIC_UPDATE); + dumpSetting(s, p, + Settings.Global.PACKAGE_VERIFIER_ENABLE, + GlobalSettingsProto.PACKAGE_VERIFIER_ENABLE); + dumpSetting(s, p, + Settings.Global.PACKAGE_VERIFIER_TIMEOUT, + GlobalSettingsProto.PACKAGE_VERIFIER_TIMEOUT); + dumpSetting(s, p, + Settings.Global.PACKAGE_VERIFIER_DEFAULT_RESPONSE, + GlobalSettingsProto.PACKAGE_VERIFIER_DEFAULT_RESPONSE); + dumpSetting(s, p, + Settings.Global.PACKAGE_VERIFIER_SETTING_VISIBLE, + GlobalSettingsProto.PACKAGE_VERIFIER_SETTING_VISIBLE); + dumpSetting(s, p, + Settings.Global.PACKAGE_VERIFIER_INCLUDE_ADB, + GlobalSettingsProto.PACKAGE_VERIFIER_INCLUDE_ADB); + dumpSetting(s, p, + Settings.Global.FSTRIM_MANDATORY_INTERVAL, + GlobalSettingsProto.FSTRIM_MANDATORY_INTERVAL); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_POLL_INTERVAL_MS, + GlobalSettingsProto.PDP_WATCHDOG_POLL_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_LONG_POLL_INTERVAL_MS, + GlobalSettingsProto.PDP_WATCHDOG_LONG_POLL_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS, + GlobalSettingsProto.PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_TRIGGER_PACKET_COUNT, + GlobalSettingsProto.PDP_WATCHDOG_TRIGGER_PACKET_COUNT); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_ERROR_POLL_COUNT, + GlobalSettingsProto.PDP_WATCHDOG_ERROR_POLL_COUNT); + dumpSetting(s, p, + Settings.Global.PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT, + GlobalSettingsProto.PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT); + dumpSetting(s, p, + Settings.Global.SAMPLING_PROFILER_MS, + GlobalSettingsProto.SAMPLING_PROFILER_MS); + dumpSetting(s, p, + Settings.Global.SETUP_PREPAID_DATA_SERVICE_URL, + GlobalSettingsProto.SETUP_PREPAID_DATA_SERVICE_URL); + dumpSetting(s, p, + Settings.Global.SETUP_PREPAID_DETECTION_TARGET_URL, + GlobalSettingsProto.SETUP_PREPAID_DETECTION_TARGET_URL); + dumpSetting(s, p, + Settings.Global.SETUP_PREPAID_DETECTION_REDIR_HOST, + GlobalSettingsProto.SETUP_PREPAID_DETECTION_REDIR_HOST); + dumpSetting(s, p, + Settings.Global.SMS_OUTGOING_CHECK_INTERVAL_MS, + GlobalSettingsProto.SMS_OUTGOING_CHECK_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT, + GlobalSettingsProto.SMS_OUTGOING_CHECK_MAX_COUNT); + dumpSetting(s, p, + Settings.Global.SMS_SHORT_CODE_CONFIRMATION, + GlobalSettingsProto.SMS_SHORT_CODE_CONFIRMATION); + dumpSetting(s, p, + Settings.Global.SMS_SHORT_CODE_RULE, + GlobalSettingsProto.SMS_SHORT_CODE_RULE); + dumpSetting(s, p, + Settings.Global.TCP_DEFAULT_INIT_RWND, + GlobalSettingsProto.TCP_DEFAULT_INIT_RWND); + dumpSetting(s, p, + Settings.Global.TETHER_SUPPORTED, + GlobalSettingsProto.TETHER_SUPPORTED); + dumpSetting(s, p, + Settings.Global.TETHER_DUN_REQUIRED, + GlobalSettingsProto.TETHER_DUN_REQUIRED); + dumpSetting(s, p, + Settings.Global.TETHER_DUN_APN, + GlobalSettingsProto.TETHER_DUN_APN); + dumpSetting(s, p, + Settings.Global.CARRIER_APP_WHITELIST, + GlobalSettingsProto.CARRIER_APP_WHITELIST); + dumpSetting(s, p, + Settings.Global.USB_MASS_STORAGE_ENABLED, + GlobalSettingsProto.USB_MASS_STORAGE_ENABLED); + dumpSetting(s, p, + Settings.Global.USE_GOOGLE_MAIL, + GlobalSettingsProto.USE_GOOGLE_MAIL); + dumpSetting(s, p, + Settings.Global.WEBVIEW_DATA_REDUCTION_PROXY_KEY, + GlobalSettingsProto.WEBVIEW_DATA_REDUCTION_PROXY_KEY); + dumpSetting(s, p, + Settings.Global.WEBVIEW_FALLBACK_LOGIC_ENABLED, + GlobalSettingsProto.WEBVIEW_FALLBACK_LOGIC_ENABLED); + dumpSetting(s, p, + Settings.Global.WEBVIEW_PROVIDER, + GlobalSettingsProto.WEBVIEW_PROVIDER); + dumpSetting(s, p, + Settings.Global.WEBVIEW_MULTIPROCESS, + GlobalSettingsProto.WEBVIEW_MULTIPROCESS); + dumpSetting(s, p, + Settings.Global.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, + GlobalSettingsProto.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT); + dumpSetting(s, p, + Settings.Global.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, + GlobalSettingsProto.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS); + dumpSetting(s, p, + Settings.Global.NETWORK_AVOID_BAD_WIFI, + GlobalSettingsProto.NETWORK_AVOID_BAD_WIFI); + dumpSetting(s, p, + Settings.Global.WIFI_DISPLAY_ON, + GlobalSettingsProto.WIFI_DISPLAY_ON); + dumpSetting(s, p, + Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON, + GlobalSettingsProto.WIFI_DISPLAY_CERTIFICATION_ON); + dumpSetting(s, p, + Settings.Global.WIFI_DISPLAY_WPS_CONFIG, + GlobalSettingsProto.WIFI_DISPLAY_WPS_CONFIG); + dumpSetting(s, p, + Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, + GlobalSettingsProto.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON); + dumpSetting(s, p, + Settings.Global.WIMAX_NETWORKS_AVAILABLE_NOTIFICATION_ON, + GlobalSettingsProto.WIMAX_NETWORKS_AVAILABLE_NOTIFICATION_ON); + dumpSetting(s, p, + Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, + GlobalSettingsProto.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY); + dumpSetting(s, p, + Settings.Global.WIFI_COUNTRY_CODE, + GlobalSettingsProto.WIFI_COUNTRY_CODE); + dumpSetting(s, p, + Settings.Global.WIFI_FRAMEWORK_SCAN_INTERVAL_MS, + GlobalSettingsProto.WIFI_FRAMEWORK_SCAN_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.WIFI_IDLE_MS, + GlobalSettingsProto.WIFI_IDLE_MS); + dumpSetting(s, p, + Settings.Global.WIFI_NUM_OPEN_NETWORKS_KEPT, + GlobalSettingsProto.WIFI_NUM_OPEN_NETWORKS_KEPT); + dumpSetting(s, p, + Settings.Global.WIFI_ON, + GlobalSettingsProto.WIFI_ON); + dumpSetting(s, p, + Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, + GlobalSettingsProto.WIFI_SCAN_ALWAYS_AVAILABLE); + dumpSetting(s, p, + Settings.Global.WIFI_WAKEUP_ENABLED, + GlobalSettingsProto.WIFI_WAKEUP_ENABLED); + dumpSetting(s, p, + Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, + GlobalSettingsProto.NETWORK_RECOMMENDATIONS_ENABLED); + dumpSetting(s, p, + Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, + GlobalSettingsProto.BLE_SCAN_ALWAYS_AVAILABLE); + dumpSetting(s, p, + Settings.Global.WIFI_SAVED_STATE, + GlobalSettingsProto.WIFI_SAVED_STATE); + dumpSetting(s, p, + Settings.Global.WIFI_SUPPLICANT_SCAN_INTERVAL_MS, + GlobalSettingsProto.WIFI_SUPPLICANT_SCAN_INTERVAL_MS); + dumpSetting(s, p, + Settings.Global.WIFI_ENHANCED_AUTO_JOIN, + GlobalSettingsProto.WIFI_ENHANCED_AUTO_JOIN); + dumpSetting(s, p, + Settings.Global.WIFI_NETWORK_SHOW_RSSI, + GlobalSettingsProto.WIFI_NETWORK_SHOW_RSSI); + dumpSetting(s, p, + Settings.Global.WIFI_SCAN_INTERVAL_WHEN_P2P_CONNECTED_MS, + GlobalSettingsProto.WIFI_SCAN_INTERVAL_WHEN_P2P_CONNECTED_MS); + dumpSetting(s, p, + Settings.Global.WIFI_WATCHDOG_ON, + GlobalSettingsProto.WIFI_WATCHDOG_ON); + dumpSetting(s, p, + Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, + GlobalSettingsProto.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED); + dumpSetting(s, p, + Settings.Global.WIFI_SUSPEND_OPTIMIZATIONS_ENABLED, + GlobalSettingsProto.WIFI_SUSPEND_OPTIMIZATIONS_ENABLED); + dumpSetting(s, p, + Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED, + GlobalSettingsProto.WIFI_VERBOSE_LOGGING_ENABLED); + dumpSetting(s, p, + Settings.Global.WIFI_MAX_DHCP_RETRY_COUNT, + GlobalSettingsProto.WIFI_MAX_DHCP_RETRY_COUNT); + dumpSetting(s, p, + Settings.Global.WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS, + GlobalSettingsProto.WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS); + dumpSetting(s, p, + Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN, + GlobalSettingsProto.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN); + dumpSetting(s, p, + Settings.Global.WIFI_FREQUENCY_BAND, + GlobalSettingsProto.WIFI_FREQUENCY_BAND); + dumpSetting(s, p, + Settings.Global.WIFI_P2P_DEVICE_NAME, + GlobalSettingsProto.WIFI_P2P_DEVICE_NAME); + dumpSetting(s, p, + Settings.Global.WIFI_REENABLE_DELAY_MS, + GlobalSettingsProto.WIFI_REENABLE_DELAY_MS); + dumpSetting(s, p, + Settings.Global.WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS, + GlobalSettingsProto.WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS); + dumpSetting(s, p, + Settings.Global.DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS, + GlobalSettingsProto.DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS); + dumpSetting(s, p, + Settings.Global.DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS, + GlobalSettingsProto.DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS); + dumpSetting(s, p, + Settings.Global.PROVISIONING_APN_ALARM_DELAY_IN_MS, + GlobalSettingsProto.PROVISIONING_APN_ALARM_DELAY_IN_MS); + dumpSetting(s, p, + Settings.Global.GPRS_REGISTER_CHECK_PERIOD_MS, + GlobalSettingsProto.GPRS_REGISTER_CHECK_PERIOD_MS); + dumpSetting(s, p, + Settings.Global.WTF_IS_FATAL, + GlobalSettingsProto.WTF_IS_FATAL); + dumpSetting(s, p, + Settings.Global.MODE_RINGER, + GlobalSettingsProto.MODE_RINGER); + dumpSetting(s, p, + Settings.Global.OVERLAY_DISPLAY_DEVICES, + GlobalSettingsProto.OVERLAY_DISPLAY_DEVICES); + dumpSetting(s, p, + Settings.Global.BATTERY_DISCHARGE_DURATION_THRESHOLD, + GlobalSettingsProto.BATTERY_DISCHARGE_DURATION_THRESHOLD); + dumpSetting(s, p, + Settings.Global.BATTERY_DISCHARGE_THRESHOLD, + GlobalSettingsProto.BATTERY_DISCHARGE_THRESHOLD); + dumpSetting(s, p, + Settings.Global.SEND_ACTION_APP_ERROR, + GlobalSettingsProto.SEND_ACTION_APP_ERROR); + dumpSetting(s, p, + Settings.Global.DROPBOX_AGE_SECONDS, + GlobalSettingsProto.DROPBOX_AGE_SECONDS); + dumpSetting(s, p, + Settings.Global.DROPBOX_MAX_FILES, + GlobalSettingsProto.DROPBOX_MAX_FILES); + dumpSetting(s, p, + Settings.Global.DROPBOX_QUOTA_KB, + GlobalSettingsProto.DROPBOX_QUOTA_KB); + dumpSetting(s, p, + Settings.Global.DROPBOX_QUOTA_PERCENT, + GlobalSettingsProto.DROPBOX_QUOTA_PERCENT); + dumpSetting(s, p, + Settings.Global.DROPBOX_RESERVE_PERCENT, + GlobalSettingsProto.DROPBOX_RESERVE_PERCENT); + dumpSetting(s, p, + Settings.Global.DROPBOX_TAG_PREFIX, + GlobalSettingsProto.DROPBOX_TAG_PREFIX); + dumpSetting(s, p, + Settings.Global.ERROR_LOGCAT_PREFIX, + GlobalSettingsProto.ERROR_LOGCAT_PREFIX); + dumpSetting(s, p, + Settings.Global.SYS_FREE_STORAGE_LOG_INTERVAL, + GlobalSettingsProto.SYS_FREE_STORAGE_LOG_INTERVAL); + dumpSetting(s, p, + Settings.Global.DISK_FREE_CHANGE_REPORTING_THRESHOLD, + GlobalSettingsProto.DISK_FREE_CHANGE_REPORTING_THRESHOLD); + dumpSetting(s, p, + Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, + GlobalSettingsProto.SYS_STORAGE_THRESHOLD_PERCENTAGE); + dumpSetting(s, p, + Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, + GlobalSettingsProto.SYS_STORAGE_THRESHOLD_MAX_BYTES); + dumpSetting(s, p, + Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES, + GlobalSettingsProto.SYS_STORAGE_FULL_THRESHOLD_BYTES); + dumpSetting(s, p, + Settings.Global.SYNC_MAX_RETRY_DELAY_IN_SECONDS, + GlobalSettingsProto.SYNC_MAX_RETRY_DELAY_IN_SECONDS); + dumpSetting(s, p, + Settings.Global.CONNECTIVITY_CHANGE_DELAY, + GlobalSettingsProto.CONNECTIVITY_CHANGE_DELAY); + dumpSetting(s, p, + Settings.Global.CONNECTIVITY_SAMPLING_INTERVAL_IN_SECONDS, + GlobalSettingsProto.CONNECTIVITY_SAMPLING_INTERVAL_IN_SECONDS); + dumpSetting(s, p, + Settings.Global.PAC_CHANGE_DELAY, + GlobalSettingsProto.PAC_CHANGE_DELAY); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_MODE, + GlobalSettingsProto.CAPTIVE_PORTAL_MODE); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_SERVER, + GlobalSettingsProto.CAPTIVE_PORTAL_SERVER); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, + GlobalSettingsProto.CAPTIVE_PORTAL_HTTPS_URL); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_HTTP_URL, + GlobalSettingsProto.CAPTIVE_PORTAL_HTTP_URL); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, + GlobalSettingsProto.CAPTIVE_PORTAL_FALLBACK_URL); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, + GlobalSettingsProto.CAPTIVE_PORTAL_USE_HTTPS); + dumpSetting(s, p, + Settings.Global.CAPTIVE_PORTAL_USER_AGENT, + GlobalSettingsProto.CAPTIVE_PORTAL_USER_AGENT); + dumpSetting(s, p, + Settings.Global.NSD_ON, + GlobalSettingsProto.NSD_ON); + dumpSetting(s, p, + Settings.Global.SET_INSTALL_LOCATION, + GlobalSettingsProto.SET_INSTALL_LOCATION); + dumpSetting(s, p, + Settings.Global.DEFAULT_INSTALL_LOCATION, + GlobalSettingsProto.DEFAULT_INSTALL_LOCATION); + dumpSetting(s, p, + Settings.Global.INET_CONDITION_DEBOUNCE_UP_DELAY, + GlobalSettingsProto.INET_CONDITION_DEBOUNCE_UP_DELAY); + dumpSetting(s, p, + Settings.Global.INET_CONDITION_DEBOUNCE_DOWN_DELAY, + GlobalSettingsProto.INET_CONDITION_DEBOUNCE_DOWN_DELAY); + dumpSetting(s, p, + Settings.Global.READ_EXTERNAL_STORAGE_ENFORCED_DEFAULT, + GlobalSettingsProto.READ_EXTERNAL_STORAGE_ENFORCED_DEFAULT); + dumpSetting(s, p, + Settings.Global.HTTP_PROXY, + GlobalSettingsProto.HTTP_PROXY); + dumpSetting(s, p, + Settings.Global.GLOBAL_HTTP_PROXY_HOST, + GlobalSettingsProto.GLOBAL_HTTP_PROXY_HOST); + dumpSetting(s, p, + Settings.Global.GLOBAL_HTTP_PROXY_PORT, + GlobalSettingsProto.GLOBAL_HTTP_PROXY_PORT); + dumpSetting(s, p, + Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST, + GlobalSettingsProto.GLOBAL_HTTP_PROXY_EXCLUSION_LIST); + dumpSetting(s, p, + Settings.Global.GLOBAL_HTTP_PROXY_PAC, + GlobalSettingsProto.GLOBAL_HTTP_PROXY_PAC); + dumpSetting(s, p, + Settings.Global.SET_GLOBAL_HTTP_PROXY, + GlobalSettingsProto.SET_GLOBAL_HTTP_PROXY); + dumpSetting(s, p, + Settings.Global.DEFAULT_DNS_SERVER, + GlobalSettingsProto.DEFAULT_DNS_SERVER); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_HEADSET_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_HEADSET_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_A2DP_SINK_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_A2DP_SINK_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_A2DP_SRC_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_A2DP_SRC_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_INPUT_DEVICE_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_INPUT_DEVICE_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_MAP_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_MAP_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_MAP_CLIENT_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_MAP_CLIENT_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_PBAP_CLIENT_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_PBAP_CLIENT_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_SAP_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_SAP_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.BLUETOOTH_PAN_PRIORITY_PREFIX, + GlobalSettingsProto.BLUETOOTH_PAN_PRIORITY_PREFIX); + dumpSetting(s, p, + Settings.Global.DEVICE_IDLE_CONSTANTS, + GlobalSettingsProto.DEVICE_IDLE_CONSTANTS); + dumpSetting(s, p, + Settings.Global.DEVICE_IDLE_CONSTANTS_WATCH, + GlobalSettingsProto.DEVICE_IDLE_CONSTANTS_WATCH); + dumpSetting(s, p, + Settings.Global.APP_IDLE_CONSTANTS, + GlobalSettingsProto.APP_IDLE_CONSTANTS); + dumpSetting(s, p, + Settings.Global.ALARM_MANAGER_CONSTANTS, + GlobalSettingsProto.ALARM_MANAGER_CONSTANTS); + dumpSetting(s, p, + Settings.Global.JOB_SCHEDULER_CONSTANTS, + GlobalSettingsProto.JOB_SCHEDULER_CONSTANTS); + dumpSetting(s, p, + Settings.Global.SHORTCUT_MANAGER_CONSTANTS, + GlobalSettingsProto.SHORTCUT_MANAGER_CONSTANTS); + dumpSetting(s, p, + Settings.Global.WINDOW_ANIMATION_SCALE, + GlobalSettingsProto.WINDOW_ANIMATION_SCALE); + dumpSetting(s, p, + Settings.Global.TRANSITION_ANIMATION_SCALE, + GlobalSettingsProto.TRANSITION_ANIMATION_SCALE); + dumpSetting(s, p, + Settings.Global.ANIMATOR_DURATION_SCALE, + GlobalSettingsProto.ANIMATOR_DURATION_SCALE); + dumpSetting(s, p, + Settings.Global.FANCY_IME_ANIMATIONS, + GlobalSettingsProto.FANCY_IME_ANIMATIONS); + dumpSetting(s, p, + Settings.Global.COMPATIBILITY_MODE, + GlobalSettingsProto.COMPATIBILITY_MODE); + dumpSetting(s, p, + Settings.Global.EMERGENCY_TONE, + GlobalSettingsProto.EMERGENCY_TONE); + dumpSetting(s, p, + Settings.Global.CALL_AUTO_RETRY, + GlobalSettingsProto.CALL_AUTO_RETRY); + dumpSetting(s, p, + Settings.Global.EMERGENCY_AFFORDANCE_NEEDED, + GlobalSettingsProto.EMERGENCY_AFFORDANCE_NEEDED); + dumpSetting(s, p, + Settings.Global.PREFERRED_NETWORK_MODE, + GlobalSettingsProto.PREFERRED_NETWORK_MODE); + dumpSetting(s, p, + Settings.Global.DEBUG_APP, + GlobalSettingsProto.DEBUG_APP); + dumpSetting(s, p, + Settings.Global.WAIT_FOR_DEBUGGER, + GlobalSettingsProto.WAIT_FOR_DEBUGGER); + dumpSetting(s, p, + Settings.Global.LOW_POWER_MODE, + GlobalSettingsProto.LOW_POWER_MODE); + dumpSetting(s, p, + Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, + GlobalSettingsProto.LOW_POWER_MODE_TRIGGER_LEVEL); + dumpSetting(s, p, + Settings.Global.ALWAYS_FINISH_ACTIVITIES, + GlobalSettingsProto.ALWAYS_FINISH_ACTIVITIES); + dumpSetting(s, p, + Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, + GlobalSettingsProto.DOCK_AUDIO_MEDIA_ENABLED); + dumpSetting(s, p, + Settings.Global.ENCODED_SURROUND_OUTPUT, + GlobalSettingsProto.ENCODED_SURROUND_OUTPUT); + dumpSetting(s, p, + Settings.Global.AUDIO_SAFE_VOLUME_STATE, + GlobalSettingsProto.AUDIO_SAFE_VOLUME_STATE); + dumpSetting(s, p, + Settings.Global.TZINFO_UPDATE_CONTENT_URL, + GlobalSettingsProto.TZINFO_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.TZINFO_UPDATE_METADATA_URL, + GlobalSettingsProto.TZINFO_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.SELINUX_UPDATE_CONTENT_URL, + GlobalSettingsProto.SELINUX_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.SELINUX_UPDATE_METADATA_URL, + GlobalSettingsProto.SELINUX_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.SMS_SHORT_CODES_UPDATE_CONTENT_URL, + GlobalSettingsProto.SMS_SHORT_CODES_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.SMS_SHORT_CODES_UPDATE_METADATA_URL, + GlobalSettingsProto.SMS_SHORT_CODES_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.APN_DB_UPDATE_CONTENT_URL, + GlobalSettingsProto.APN_DB_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.APN_DB_UPDATE_METADATA_URL, + GlobalSettingsProto.APN_DB_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.CERT_PIN_UPDATE_CONTENT_URL, + GlobalSettingsProto.CERT_PIN_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.CERT_PIN_UPDATE_METADATA_URL, + GlobalSettingsProto.CERT_PIN_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.INTENT_FIREWALL_UPDATE_CONTENT_URL, + GlobalSettingsProto.INTENT_FIREWALL_UPDATE_CONTENT_URL); + dumpSetting(s, p, + Settings.Global.INTENT_FIREWALL_UPDATE_METADATA_URL, + GlobalSettingsProto.INTENT_FIREWALL_UPDATE_METADATA_URL); + dumpSetting(s, p, + Settings.Global.SELINUX_STATUS, + GlobalSettingsProto.SELINUX_STATUS); + dumpSetting(s, p, + Settings.Global.DEVELOPMENT_FORCE_RTL, + GlobalSettingsProto.DEVELOPMENT_FORCE_RTL); + dumpSetting(s, p, + Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, + GlobalSettingsProto.LOW_BATTERY_SOUND_TIMEOUT); + dumpSetting(s, p, + Settings.Global.WIFI_BOUNCE_DELAY_OVERRIDE_MS, + GlobalSettingsProto.WIFI_BOUNCE_DELAY_OVERRIDE_MS); + dumpSetting(s, p, + Settings.Global.POLICY_CONTROL, + GlobalSettingsProto.POLICY_CONTROL); + dumpSetting(s, p, + Settings.Global.ZEN_MODE, + GlobalSettingsProto.ZEN_MODE); + dumpSetting(s, p, + Settings.Global.ZEN_MODE_RINGER_LEVEL, + GlobalSettingsProto.ZEN_MODE_RINGER_LEVEL); + dumpSetting(s, p, + Settings.Global.ZEN_MODE_CONFIG_ETAG, + GlobalSettingsProto.ZEN_MODE_CONFIG_ETAG); + dumpSetting(s, p, + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + GlobalSettingsProto.HEADS_UP_NOTIFICATIONS_ENABLED); + dumpSetting(s, p, + Settings.Global.DEVICE_NAME, + GlobalSettingsProto.DEVICE_NAME); + dumpSetting(s, p, + Settings.Global.NETWORK_SCORING_PROVISIONED, + GlobalSettingsProto.NETWORK_SCORING_PROVISIONED); + dumpSetting(s, p, + Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, + GlobalSettingsProto.REQUIRE_PASSWORD_TO_DECRYPT); + dumpSetting(s, p, + Settings.Global.ENHANCED_4G_MODE_ENABLED, + GlobalSettingsProto.ENHANCED_4G_MODE_ENABLED); + dumpSetting(s, p, + Settings.Global.VT_IMS_ENABLED, + GlobalSettingsProto.VT_IMS_ENABLED); + dumpSetting(s, p, + Settings.Global.WFC_IMS_ENABLED, + GlobalSettingsProto.WFC_IMS_ENABLED); + dumpSetting(s, p, + Settings.Global.WFC_IMS_MODE, + GlobalSettingsProto.WFC_IMS_MODE); + dumpSetting(s, p, + Settings.Global.WFC_IMS_ROAMING_MODE, + GlobalSettingsProto.WFC_IMS_ROAMING_MODE); + dumpSetting(s, p, + Settings.Global.WFC_IMS_ROAMING_ENABLED, + GlobalSettingsProto.WFC_IMS_ROAMING_ENABLED); + dumpSetting(s, p, + Settings.Global.LTE_SERVICE_FORCED, + GlobalSettingsProto.LTE_SERVICE_FORCED); + dumpSetting(s, p, + Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES, + GlobalSettingsProto.EPHEMERAL_COOKIE_MAX_SIZE_BYTES); + dumpSetting(s, p, + Settings.Global.ENABLE_EPHEMERAL_FEATURE, + GlobalSettingsProto.ENABLE_EPHEMERAL_FEATURE); + dumpSetting(s, p, + Settings.Global.UNINSTALLED_EPHEMERAL_APP_CACHE_DURATION_MILLIS, + GlobalSettingsProto.UNINSTALLED_EPHEMERAL_APP_CACHE_DURATION_MILLIS); + dumpSetting(s, p, + Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, + GlobalSettingsProto.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED); + dumpSetting(s, p, + Settings.Global.BOOT_COUNT, + GlobalSettingsProto.BOOT_COUNT); + dumpSetting(s, p, + Settings.Global.SAFE_BOOT_DISALLOWED, + GlobalSettingsProto.SAFE_BOOT_DISALLOWED); + dumpSetting(s, p, + Settings.Global.DEVICE_DEMO_MODE, + GlobalSettingsProto.DEVICE_DEMO_MODE); + dumpSetting(s, p, + Settings.Global.RETAIL_DEMO_MODE_CONSTANTS, + GlobalSettingsProto.RETAIL_DEMO_MODE_CONSTANTS); + dumpSetting(s, p, + Settings.Global.DATABASE_DOWNGRADE_REASON, + GlobalSettingsProto.DATABASE_DOWNGRADE_REASON); + dumpSetting(s, p, + Settings.Global.CONTACTS_DATABASE_WAL_ENABLED, + GlobalSettingsProto.CONTACTS_DATABASE_WAL_ENABLED); + dumpSetting(s, p, + Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION, + GlobalSettingsProto.MULTI_SIM_VOICE_CALL_SUBSCRIPTION); + dumpSetting(s, p, + Settings.Global.MULTI_SIM_VOICE_PROMPT, + GlobalSettingsProto.MULTI_SIM_VOICE_PROMPT); + dumpSetting(s, p, + Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION, + GlobalSettingsProto.MULTI_SIM_DATA_CALL_SUBSCRIPTION); + dumpSetting(s, p, + Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION, + GlobalSettingsProto.MULTI_SIM_SMS_SUBSCRIPTION); + dumpSetting(s, p, + Settings.Global.MULTI_SIM_SMS_PROMPT, + GlobalSettingsProto.MULTI_SIM_SMS_PROMPT); + dumpSetting(s, p, + Settings.Global.NEW_CONTACT_AGGREGATOR, + GlobalSettingsProto.NEW_CONTACT_AGGREGATOR); + dumpSetting(s, p, + Settings.Global.CONTACT_METADATA_SYNC_ENABLED, + GlobalSettingsProto.CONTACT_METADATA_SYNC_ENABLED); + dumpSetting(s, p, + Settings.Global.ENABLE_CELLULAR_ON_BOOT, + GlobalSettingsProto.ENABLE_CELLULAR_ON_BOOT); + dumpSetting(s, p, + Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, + GlobalSettingsProto.MAX_NOTIFICATION_ENQUEUE_RATE); + dumpSetting(s, p, + Settings.Global.CELL_ON, + GlobalSettingsProto.CELL_ON); + } + + /** Dump a single {@link SettingsState.Setting} to a proto buf */ + private static void dumpSetting(@NonNull SettingsState settings, + @NonNull ProtoOutputStream proto, String settingName, long fieldId) { + SettingsState.Setting setting = settings.getSettingLocked(settingName); + long settingsToken = proto.start(fieldId); + proto.write(SettingProto.ID, setting.getId()); + proto.write(SettingProto.NAME, settingName); + if (setting.getPackageName() != null) { + proto.write(SettingProto.PKG, setting.getPackageName()); + } + proto.write(SettingProto.VALUE, setting.getValue()); + if (setting.getDefaultValue() != null) { + proto.write(SettingProto.DEFAULT_VALUE, setting.getDefaultValue()); + proto.write(SettingProto.DEFAULT_FROM_SYSTEM, setting.isDefaultFromSystem()); + } + proto.end(settingsToken); + } + + static void dumpProtoSecureSettingsLocked( + @NonNull SettingsState s, @NonNull ProtoOutputStream p) { + dumpSetting(s, p, + Settings.Secure.ANDROID_ID, + SecureSettingsProto.ANDROID_ID); + dumpSetting(s, p, + Settings.Secure.DEFAULT_INPUT_METHOD, + SecureSettingsProto.DEFAULT_INPUT_METHOD); + dumpSetting(s, p, + Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, + SecureSettingsProto.SELECTED_INPUT_METHOD_SUBTYPE); + dumpSetting(s, p, + Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, + SecureSettingsProto.INPUT_METHODS_SUBTYPE_HISTORY); + dumpSetting(s, p, + Settings.Secure.INPUT_METHOD_SELECTOR_VISIBILITY, + SecureSettingsProto.INPUT_METHOD_SELECTOR_VISIBILITY); + dumpSetting(s, p, + Settings.Secure.VOICE_INTERACTION_SERVICE, + SecureSettingsProto.VOICE_INTERACTION_SERVICE); + dumpSetting(s, p, + Settings.Secure.AUTO_FILL_SERVICE, + SecureSettingsProto.AUTO_FILL_SERVICE); + dumpSetting(s, p, + Settings.Secure.BLUETOOTH_HCI_LOG, + SecureSettingsProto.BLUETOOTH_HCI_LOG); + dumpSetting(s, p, + Settings.Secure.USER_SETUP_COMPLETE, + SecureSettingsProto.USER_SETUP_COMPLETE); + dumpSetting(s, p, + Settings.Secure.COMPLETED_CATEGORY_PREFIX, + SecureSettingsProto.COMPLETED_CATEGORY_PREFIX); + dumpSetting(s, p, + Settings.Secure.ENABLED_INPUT_METHODS, + SecureSettingsProto.ENABLED_INPUT_METHODS); + dumpSetting(s, p, + Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, + SecureSettingsProto.DISABLED_SYSTEM_INPUT_METHODS); + dumpSetting(s, p, + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, + SecureSettingsProto.SHOW_IME_WITH_HARD_KEYBOARD); + dumpSetting(s, p, + Settings.Secure.ALWAYS_ON_VPN_APP, + SecureSettingsProto.ALWAYS_ON_VPN_APP); + dumpSetting(s, p, + Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, + SecureSettingsProto.ALWAYS_ON_VPN_LOCKDOWN); + dumpSetting(s, p, + Settings.Secure.INSTALL_NON_MARKET_APPS, + SecureSettingsProto.INSTALL_NON_MARKET_APPS); + dumpSetting(s, p, + Settings.Secure.LOCATION_MODE, + SecureSettingsProto.LOCATION_MODE); + dumpSetting(s, p, + Settings.Secure.LOCATION_PREVIOUS_MODE, + SecureSettingsProto.LOCATION_PREVIOUS_MODE); + dumpSetting(s, p, + Settings.Secure.LOCK_TO_APP_EXIT_LOCKED, + SecureSettingsProto.LOCK_TO_APP_EXIT_LOCKED); + dumpSetting(s, p, + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + SecureSettingsProto.LOCK_SCREEN_LOCK_AFTER_TIMEOUT); + dumpSetting(s, p, + Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT, + SecureSettingsProto.LOCK_SCREEN_ALLOW_REMOTE_INPUT); + dumpSetting(s, p, + Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, + SecureSettingsProto.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING); + dumpSetting(s, p, + Settings.Secure.TRUST_AGENTS_INITIALIZED, + SecureSettingsProto.TRUST_AGENTS_INITIALIZED); + dumpSetting(s, p, + Settings.Secure.PARENTAL_CONTROL_ENABLED, + SecureSettingsProto.PARENTAL_CONTROL_ENABLED); + dumpSetting(s, p, + Settings.Secure.PARENTAL_CONTROL_LAST_UPDATE, + SecureSettingsProto.PARENTAL_CONTROL_LAST_UPDATE); + dumpSetting(s, p, + Settings.Secure.PARENTAL_CONTROL_REDIRECT_URL, + SecureSettingsProto.PARENTAL_CONTROL_REDIRECT_URL); + dumpSetting(s, p, + Settings.Secure.SETTINGS_CLASSNAME, + SecureSettingsProto.SETTINGS_CLASSNAME); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_ENABLED, + SecureSettingsProto.ACCESSIBILITY_ENABLED); + dumpSetting(s, p, + Settings.Secure.TOUCH_EXPLORATION_ENABLED, + SecureSettingsProto.TOUCH_EXPLORATION_ENABLED); + dumpSetting(s, p, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + SecureSettingsProto.ENABLED_ACCESSIBILITY_SERVICES); + dumpSetting(s, p, + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + SecureSettingsProto.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, + SecureSettingsProto.ACCESSIBILITY_SPEAK_PASSWORD); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, + SecureSettingsProto.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + SecureSettingsProto.ACCESSIBILITY_SCRIPT_INJECTION); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL, + SecureSettingsProto.ACCESSIBILITY_SCREEN_READER_URL); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS, + SecureSettingsProto.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + SecureSettingsProto.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + SecureSettingsProto.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, + SecureSettingsProto.ACCESSIBILITY_SOFT_KEYBOARD_MODE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_LOCALE, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_LOCALE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_PRESET); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_EDGE_TYPE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_EDGE_COLOR); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_TYPEFACE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, + SecureSettingsProto.ACCESSIBILITY_CAPTIONING_FONT_SCALE); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, + SecureSettingsProto.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + SecureSettingsProto.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, + SecureSettingsProto.ACCESSIBILITY_DISPLAY_DALTONIZER); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, + SecureSettingsProto.ACCESSIBILITY_AUTOCLICK_ENABLED); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, + SecureSettingsProto.ACCESSIBILITY_AUTOCLICK_DELAY); + dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, + SecureSettingsProto.ACCESSIBILITY_LARGE_POINTER_ICON); + dumpSetting(s, p, + Settings.Secure.LONG_PRESS_TIMEOUT, + SecureSettingsProto.LONG_PRESS_TIMEOUT); + dumpSetting(s, p, + Settings.Secure.MULTI_PRESS_TIMEOUT, + SecureSettingsProto.MULTI_PRESS_TIMEOUT); + dumpSetting(s, p, + Settings.Secure.ENABLED_PRINT_SERVICES, + SecureSettingsProto.ENABLED_PRINT_SERVICES); + dumpSetting(s, p, + Settings.Secure.DISABLED_PRINT_SERVICES, + SecureSettingsProto.DISABLED_PRINT_SERVICES); + dumpSetting(s, p, + Settings.Secure.DISPLAY_DENSITY_FORCED, + SecureSettingsProto.DISPLAY_DENSITY_FORCED); + dumpSetting(s, p, + Settings.Secure.TTS_DEFAULT_RATE, + SecureSettingsProto.TTS_DEFAULT_RATE); + dumpSetting(s, p, + Settings.Secure.TTS_DEFAULT_PITCH, + SecureSettingsProto.TTS_DEFAULT_PITCH); + dumpSetting(s, p, + Settings.Secure.TTS_DEFAULT_SYNTH, + SecureSettingsProto.TTS_DEFAULT_SYNTH); + dumpSetting(s, p, + Settings.Secure.TTS_DEFAULT_LOCALE, + SecureSettingsProto.TTS_DEFAULT_LOCALE); + dumpSetting(s, p, + Settings.Secure.TTS_ENABLED_PLUGINS, + SecureSettingsProto.TTS_ENABLED_PLUGINS); + dumpSetting(s, p, + Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, + SecureSettingsProto.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS); + dumpSetting(s, p, + Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS, + SecureSettingsProto.ALLOWED_GEOLOCATION_ORIGINS); + dumpSetting(s, p, + Settings.Secure.PREFERRED_TTY_MODE, + SecureSettingsProto.PREFERRED_TTY_MODE); + dumpSetting(s, p, + Settings.Secure.ENHANCED_VOICE_PRIVACY_ENABLED, + SecureSettingsProto.ENHANCED_VOICE_PRIVACY_ENABLED); + dumpSetting(s, p, + Settings.Secure.TTY_MODE_ENABLED, + SecureSettingsProto.TTY_MODE_ENABLED); + dumpSetting(s, p, + Settings.Secure.BACKUP_ENABLED, + SecureSettingsProto.BACKUP_ENABLED); + dumpSetting(s, p, + Settings.Secure.BACKUP_AUTO_RESTORE, + SecureSettingsProto.BACKUP_AUTO_RESTORE); + dumpSetting(s, p, + Settings.Secure.BACKUP_PROVISIONED, + SecureSettingsProto.BACKUP_PROVISIONED); + dumpSetting(s, p, + Settings.Secure.BACKUP_TRANSPORT, + SecureSettingsProto.BACKUP_TRANSPORT); + dumpSetting(s, p, + Settings.Secure.LAST_SETUP_SHOWN, + SecureSettingsProto.LAST_SETUP_SHOWN); + dumpSetting(s, p, + Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY, + SecureSettingsProto.SEARCH_GLOBAL_SEARCH_ACTIVITY); + dumpSetting(s, p, + Settings.Secure.SEARCH_NUM_PROMOTED_SOURCES, + SecureSettingsProto.SEARCH_NUM_PROMOTED_SOURCES); + dumpSetting(s, p, + Settings.Secure.SEARCH_MAX_RESULTS_TO_DISPLAY, + SecureSettingsProto.SEARCH_MAX_RESULTS_TO_DISPLAY); + dumpSetting(s, p, + Settings.Secure.SEARCH_MAX_RESULTS_PER_SOURCE, + SecureSettingsProto.SEARCH_MAX_RESULTS_PER_SOURCE); + dumpSetting(s, p, + Settings.Secure.SEARCH_WEB_RESULTS_OVERRIDE_LIMIT, + SecureSettingsProto.SEARCH_WEB_RESULTS_OVERRIDE_LIMIT); + dumpSetting(s, p, + Settings.Secure.SEARCH_PROMOTED_SOURCE_DEADLINE_MILLIS, + SecureSettingsProto.SEARCH_PROMOTED_SOURCE_DEADLINE_MILLIS); + dumpSetting(s, p, + Settings.Secure.SEARCH_SOURCE_TIMEOUT_MILLIS, + SecureSettingsProto.SEARCH_SOURCE_TIMEOUT_MILLIS); + dumpSetting(s, p, + Settings.Secure.SEARCH_PREFILL_MILLIS, + SecureSettingsProto.SEARCH_PREFILL_MILLIS); + dumpSetting(s, p, + Settings.Secure.SEARCH_MAX_STAT_AGE_MILLIS, + SecureSettingsProto.SEARCH_MAX_STAT_AGE_MILLIS); + dumpSetting(s, p, + Settings.Secure.SEARCH_MAX_SOURCE_EVENT_AGE_MILLIS, + SecureSettingsProto.SEARCH_MAX_SOURCE_EVENT_AGE_MILLIS); + dumpSetting(s, p, + Settings.Secure.SEARCH_MIN_IMPRESSIONS_FOR_SOURCE_RANKING, + SecureSettingsProto.SEARCH_MIN_IMPRESSIONS_FOR_SOURCE_RANKING); + dumpSetting(s, p, + Settings.Secure.SEARCH_MIN_CLICKS_FOR_SOURCE_RANKING, + SecureSettingsProto.SEARCH_MIN_CLICKS_FOR_SOURCE_RANKING); + dumpSetting(s, p, + Settings.Secure.SEARCH_MAX_SHORTCUTS_RETURNED, + SecureSettingsProto.SEARCH_MAX_SHORTCUTS_RETURNED); + dumpSetting(s, p, + Settings.Secure.SEARCH_QUERY_THREAD_CORE_POOL_SIZE, + SecureSettingsProto.SEARCH_QUERY_THREAD_CORE_POOL_SIZE); + dumpSetting(s, p, + Settings.Secure.SEARCH_QUERY_THREAD_MAX_POOL_SIZE, + SecureSettingsProto.SEARCH_QUERY_THREAD_MAX_POOL_SIZE); + dumpSetting(s, p, + Settings.Secure.SEARCH_SHORTCUT_REFRESH_CORE_POOL_SIZE, + SecureSettingsProto.SEARCH_SHORTCUT_REFRESH_CORE_POOL_SIZE); + dumpSetting(s, p, + Settings.Secure.SEARCH_SHORTCUT_REFRESH_MAX_POOL_SIZE, + SecureSettingsProto.SEARCH_SHORTCUT_REFRESH_MAX_POOL_SIZE); + dumpSetting(s, p, + Settings.Secure.SEARCH_THREAD_KEEPALIVE_SECONDS, + SecureSettingsProto.SEARCH_THREAD_KEEPALIVE_SECONDS); + dumpSetting(s, p, + Settings.Secure.SEARCH_PER_SOURCE_CONCURRENT_QUERY_LIMIT, + SecureSettingsProto.SEARCH_PER_SOURCE_CONCURRENT_QUERY_LIMIT); + dumpSetting(s, p, + Settings.Secure.MOUNT_PLAY_NOTIFICATION_SND, + SecureSettingsProto.MOUNT_PLAY_NOTIFICATION_SND); + dumpSetting(s, p, + Settings.Secure.MOUNT_UMS_AUTOSTART, + SecureSettingsProto.MOUNT_UMS_AUTOSTART); + dumpSetting(s, p, + Settings.Secure.MOUNT_UMS_PROMPT, + SecureSettingsProto.MOUNT_UMS_PROMPT); + dumpSetting(s, p, + Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, + SecureSettingsProto.MOUNT_UMS_NOTIFY_ENABLED); + dumpSetting(s, p, + Settings.Secure.ANR_SHOW_BACKGROUND, + SecureSettingsProto.ANR_SHOW_BACKGROUND); + dumpSetting(s, p, + Settings.Secure.VOICE_RECOGNITION_SERVICE, + SecureSettingsProto.VOICE_RECOGNITION_SERVICE); + dumpSetting(s, p, + Settings.Secure.PACKAGE_VERIFIER_USER_CONSENT, + SecureSettingsProto.PACKAGE_VERIFIER_USER_CONSENT); + dumpSetting(s, p, + Settings.Secure.SELECTED_SPELL_CHECKER, + SecureSettingsProto.SELECTED_SPELL_CHECKER); + dumpSetting(s, p, + Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, + SecureSettingsProto.SELECTED_SPELL_CHECKER_SUBTYPE); + dumpSetting(s, p, + Settings.Secure.SPELL_CHECKER_ENABLED, + SecureSettingsProto.SPELL_CHECKER_ENABLED); + dumpSetting(s, p, + Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, + SecureSettingsProto.INCALL_POWER_BUTTON_BEHAVIOR); + dumpSetting(s, p, + Settings.Secure.INCALL_BACK_BUTTON_BEHAVIOR, + SecureSettingsProto.INCALL_BACK_BUTTON_BEHAVIOR); + dumpSetting(s, p, + Settings.Secure.WAKE_GESTURE_ENABLED, + SecureSettingsProto.WAKE_GESTURE_ENABLED); + dumpSetting(s, p, + Settings.Secure.DOZE_ENABLED, + SecureSettingsProto.DOZE_ENABLED); + dumpSetting(s, p, + Settings.Secure.DOZE_ALWAYS_ON, + SecureSettingsProto.DOZE_ALWAYS_ON); + dumpSetting(s, p, + Settings.Secure.DOZE_PULSE_ON_PICK_UP, + SecureSettingsProto.DOZE_PULSE_ON_PICK_UP); + dumpSetting(s, p, + Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP, + SecureSettingsProto.DOZE_PULSE_ON_DOUBLE_TAP); + dumpSetting(s, p, + Settings.Secure.UI_NIGHT_MODE, + SecureSettingsProto.UI_NIGHT_MODE); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ENABLED, + SecureSettingsProto.SCREENSAVER_ENABLED); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_COMPONENTS, + SecureSettingsProto.SCREENSAVER_COMPONENTS); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + SecureSettingsProto.SCREENSAVER_ACTIVATE_ON_DOCK); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + SecureSettingsProto.SCREENSAVER_ACTIVATE_ON_SLEEP); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, + SecureSettingsProto.SCREENSAVER_DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, + SecureSettingsProto.NFC_PAYMENT_DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.NFC_PAYMENT_FOREGROUND, + SecureSettingsProto.NFC_PAYMENT_FOREGROUND); + dumpSetting(s, p, + Settings.Secure.SMS_DEFAULT_APPLICATION, + SecureSettingsProto.SMS_DEFAULT_APPLICATION); + dumpSetting(s, p, + Settings.Secure.DIALER_DEFAULT_APPLICATION, + SecureSettingsProto.DIALER_DEFAULT_APPLICATION); + dumpSetting(s, p, + Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION, + SecureSettingsProto.EMERGENCY_ASSISTANCE_APPLICATION); + dumpSetting(s, p, + Settings.Secure.ASSIST_STRUCTURE_ENABLED, + SecureSettingsProto.ASSIST_STRUCTURE_ENABLED); + dumpSetting(s, p, + Settings.Secure.ASSIST_SCREENSHOT_ENABLED, + SecureSettingsProto.ASSIST_SCREENSHOT_ENABLED); + dumpSetting(s, p, + Settings.Secure.ASSIST_DISCLOSURE_ENABLED, + SecureSettingsProto.ASSIST_DISCLOSURE_ENABLED); + dumpSetting(s, p, + Settings.Secure.ENABLED_NOTIFICATION_ASSISTANT, + SecureSettingsProto.ENABLED_NOTIFICATION_ASSISTANT); + dumpSetting(s, p, + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + SecureSettingsProto.ENABLED_NOTIFICATION_LISTENERS); + dumpSetting(s, p, + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES, + SecureSettingsProto.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES); + dumpSetting(s, p, + Settings.Secure.SYNC_PARENT_SOUNDS, + SecureSettingsProto.SYNC_PARENT_SOUNDS); + dumpSetting(s, p, + Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, + SecureSettingsProto.IMMERSIVE_MODE_CONFIRMATIONS); + dumpSetting(s, p, + Settings.Secure.PRINT_SERVICE_SEARCH_URI, + SecureSettingsProto.PRINT_SERVICE_SEARCH_URI); + dumpSetting(s, p, + Settings.Secure.PAYMENT_SERVICE_SEARCH_URI, + SecureSettingsProto.PAYMENT_SERVICE_SEARCH_URI); + dumpSetting(s, p, + Settings.Secure.SKIP_FIRST_USE_HINTS, + SecureSettingsProto.SKIP_FIRST_USE_HINTS); + dumpSetting(s, p, + Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, + SecureSettingsProto.UNSAFE_VOLUME_MUSIC_ACTIVE_MS); + dumpSetting(s, p, + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, + SecureSettingsProto.LOCK_SCREEN_SHOW_NOTIFICATIONS); + dumpSetting(s, p, + Settings.Secure.TV_INPUT_HIDDEN_INPUTS, + SecureSettingsProto.TV_INPUT_HIDDEN_INPUTS); + dumpSetting(s, p, + Settings.Secure.TV_INPUT_CUSTOM_LABELS, + SecureSettingsProto.TV_INPUT_CUSTOM_LABELS); + dumpSetting(s, p, + Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, + SecureSettingsProto.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED); + dumpSetting(s, p, + Settings.Secure.SLEEP_TIMEOUT, + SecureSettingsProto.SLEEP_TIMEOUT); + dumpSetting(s, p, + Settings.Secure.DOUBLE_TAP_TO_WAKE, + SecureSettingsProto.DOUBLE_TAP_TO_WAKE); + dumpSetting(s, p, + Settings.Secure.ASSISTANT, + SecureSettingsProto.ASSISTANT); + dumpSetting(s, p, + Settings.Secure.CAMERA_GESTURE_DISABLED, + SecureSettingsProto.CAMERA_GESTURE_DISABLED); + dumpSetting(s, p, + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, + SecureSettingsProto.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED); + dumpSetting(s, p, + Settings.Secure.CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED, + SecureSettingsProto.CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED); + dumpSetting(s, p, + Settings.Secure.NIGHT_DISPLAY_ACTIVATED, + SecureSettingsProto.NIGHT_DISPLAY_ACTIVATED); + dumpSetting(s, p, + Settings.Secure.NIGHT_DISPLAY_AUTO_MODE, + SecureSettingsProto.NIGHT_DISPLAY_AUTO_MODE); + dumpSetting(s, p, + Settings.Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, + SecureSettingsProto.NIGHT_DISPLAY_CUSTOM_START_TIME); + dumpSetting(s, p, + Settings.Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, + SecureSettingsProto.NIGHT_DISPLAY_CUSTOM_END_TIME); + dumpSetting(s, p, + Settings.Secure.BRIGHTNESS_USE_TWILIGHT, + SecureSettingsProto.BRIGHTNESS_USE_TWILIGHT); + dumpSetting(s, p, + Settings.Secure.ENABLED_VR_LISTENERS, + SecureSettingsProto.ENABLED_VR_LISTENERS); + dumpSetting(s, p, + Settings.Secure.VR_DISPLAY_MODE, + SecureSettingsProto.VR_DISPLAY_MODE); + dumpSetting(s, p, + Settings.Secure.CARRIER_APPS_HANDLED, + SecureSettingsProto.CARRIER_APPS_HANDLED); + dumpSetting(s, p, + Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, + SecureSettingsProto.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH); + dumpSetting(s, p, + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, + SecureSettingsProto.AUTOMATIC_STORAGE_MANAGER_ENABLED); + dumpSetting(s, p, + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN, + SecureSettingsProto.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN); + dumpSetting(s, p, + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_BYTES_CLEARED, + SecureSettingsProto.AUTOMATIC_STORAGE_MANAGER_BYTES_CLEARED); + dumpSetting(s, p, + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, + SecureSettingsProto.AUTOMATIC_STORAGE_MANAGER_LAST_RUN); + dumpSetting(s, p, + Settings.Secure.SYSTEM_NAVIGATION_KEYS_ENABLED, + SecureSettingsProto.SYSTEM_NAVIGATION_KEYS_ENABLED); + dumpSetting(s, p, + Settings.Secure.DOWNLOADS_BACKUP_ENABLED, + SecureSettingsProto.DOWNLOADS_BACKUP_ENABLED); + dumpSetting(s, p, + Settings.Secure.DOWNLOADS_BACKUP_ALLOW_METERED, + SecureSettingsProto.DOWNLOADS_BACKUP_ALLOW_METERED); + dumpSetting(s, p, + Settings.Secure.DOWNLOADS_BACKUP_CHARGING_ONLY, + SecureSettingsProto.DOWNLOADS_BACKUP_CHARGING_ONLY); + dumpSetting(s, p, + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DOWNLOADS_DAYS_TO_RETAIN, + SecureSettingsProto.AUTOMATIC_STORAGE_MANAGER_DOWNLOADS_DAYS_TO_RETAIN); + dumpSetting(s, p, + Settings.Secure.QS_TILES, + SecureSettingsProto.QS_TILES); + dumpSetting(s, p, + Settings.Secure.DEMO_USER_SETUP_COMPLETE, + SecureSettingsProto.DEMO_USER_SETUP_COMPLETE); + dumpSetting(s, p, + Settings.Secure.WEB_ACTION_ENABLED, + SecureSettingsProto.WEB_ACTION_ENABLED); + dumpSetting(s, p, + Settings.Secure.DEVICE_PAIRED, + SecureSettingsProto.DEVICE_PAIRED); + } + + private static void dumpProtoSystemSettingsLocked( + @NonNull SettingsState s, @NonNull ProtoOutputStream p) { + dumpSetting(s, p, + Settings.System.END_BUTTON_BEHAVIOR, + SystemSettingsProto.END_BUTTON_BEHAVIOR); + dumpSetting(s, p, + Settings.System.ADVANCED_SETTINGS, + SystemSettingsProto.ADVANCED_SETTINGS); + dumpSetting(s, p, + Settings.System.BLUETOOTH_DISCOVERABILITY, + SystemSettingsProto.BLUETOOTH_DISCOVERABILITY); + dumpSetting(s, p, + Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT, + SystemSettingsProto.BLUETOOTH_DISCOVERABILITY_TIMEOUT); + dumpSetting(s, p, + Settings.System.FONT_SCALE, + SystemSettingsProto.FONT_SCALE); + dumpSetting(s, p, + Settings.System.SYSTEM_LOCALES, + SystemSettingsProto.SYSTEM_LOCALES); + dumpSetting(s, p, + Settings.System.SCREEN_OFF_TIMEOUT, + SystemSettingsProto.SCREEN_OFF_TIMEOUT); + dumpSetting(s, p, + Settings.System.SCREEN_BRIGHTNESS, + SystemSettingsProto.SCREEN_BRIGHTNESS); + dumpSetting(s, p, + Settings.System.SCREEN_BRIGHTNESS_FOR_VR, + SystemSettingsProto.SCREEN_BRIGHTNESS_FOR_VR); + dumpSetting(s, p, + Settings.System.SCREEN_BRIGHTNESS_MODE, + SystemSettingsProto.SCREEN_BRIGHTNESS_MODE); + dumpSetting(s, p, + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, + SystemSettingsProto.SCREEN_AUTO_BRIGHTNESS_ADJ); + dumpSetting(s, p, + Settings.System.MODE_RINGER_STREAMS_AFFECTED, + SystemSettingsProto.MODE_RINGER_STREAMS_AFFECTED); + dumpSetting(s, p, + Settings.System.MUTE_STREAMS_AFFECTED, + SystemSettingsProto.MUTE_STREAMS_AFFECTED); + dumpSetting(s, p, + Settings.System.VIBRATE_ON, + SystemSettingsProto.VIBRATE_ON); + dumpSetting(s, p, + Settings.System.VIBRATE_INPUT_DEVICES, + SystemSettingsProto.VIBRATE_INPUT_DEVICES); + dumpSetting(s, p, + Settings.System.VOLUME_RING, + SystemSettingsProto.VOLUME_RING); + dumpSetting(s, p, + Settings.System.VOLUME_SYSTEM, + SystemSettingsProto.VOLUME_SYSTEM); + dumpSetting(s, p, + Settings.System.VOLUME_VOICE, + SystemSettingsProto.VOLUME_VOICE); + dumpSetting(s, p, + Settings.System.VOLUME_MUSIC, + SystemSettingsProto.VOLUME_MUSIC); + dumpSetting(s, p, + Settings.System.VOLUME_ALARM, + SystemSettingsProto.VOLUME_ALARM); + dumpSetting(s, p, + Settings.System.VOLUME_NOTIFICATION, + SystemSettingsProto.VOLUME_NOTIFICATION); + dumpSetting(s, p, + Settings.System.VOLUME_BLUETOOTH_SCO, + SystemSettingsProto.VOLUME_BLUETOOTH_SCO); + dumpSetting(s, p, + Settings.System.VOLUME_MASTER, + SystemSettingsProto.VOLUME_MASTER); + dumpSetting(s, p, + Settings.System.MASTER_MONO, + SystemSettingsProto.MASTER_MONO); + dumpSetting(s, p, + Settings.System.VIBRATE_IN_SILENT, + SystemSettingsProto.VIBRATE_IN_SILENT); + dumpSetting(s, p, + Settings.System.APPEND_FOR_LAST_AUDIBLE, + SystemSettingsProto.APPEND_FOR_LAST_AUDIBLE); + dumpSetting(s, p, + Settings.System.RINGTONE, + SystemSettingsProto.RINGTONE); + dumpSetting(s, p, + Settings.System.RINGTONE_CACHE, + SystemSettingsProto.RINGTONE_CACHE); + dumpSetting(s, p, + Settings.System.NOTIFICATION_SOUND, + SystemSettingsProto.NOTIFICATION_SOUND); + dumpSetting(s, p, + Settings.System.NOTIFICATION_SOUND_CACHE, + SystemSettingsProto.NOTIFICATION_SOUND_CACHE); + dumpSetting(s, p, + Settings.System.ALARM_ALERT, + SystemSettingsProto.ALARM_ALERT); + dumpSetting(s, p, + Settings.System.ALARM_ALERT_CACHE, + SystemSettingsProto.ALARM_ALERT_CACHE); + dumpSetting(s, p, + Settings.System.MEDIA_BUTTON_RECEIVER, + SystemSettingsProto.MEDIA_BUTTON_RECEIVER); + dumpSetting(s, p, + Settings.System.TEXT_AUTO_REPLACE, + SystemSettingsProto.TEXT_AUTO_REPLACE); + dumpSetting(s, p, + Settings.System.TEXT_AUTO_CAPS, + SystemSettingsProto.TEXT_AUTO_CAPS); + dumpSetting(s, p, + Settings.System.TEXT_AUTO_PUNCTUATE, + SystemSettingsProto.TEXT_AUTO_PUNCTUATE); + dumpSetting(s, p, + Settings.System.TEXT_SHOW_PASSWORD, + SystemSettingsProto.TEXT_SHOW_PASSWORD); + dumpSetting(s, p, + Settings.System.SHOW_GTALK_SERVICE_STATUS, + SystemSettingsProto.SHOW_GTALK_SERVICE_STATUS); + dumpSetting(s, p, + Settings.System.TIME_12_24, + SystemSettingsProto.TIME_12_24); + dumpSetting(s, p, + Settings.System.DATE_FORMAT, + SystemSettingsProto.DATE_FORMAT); + dumpSetting(s, p, + Settings.System.SETUP_WIZARD_HAS_RUN, + SystemSettingsProto.SETUP_WIZARD_HAS_RUN); + dumpSetting(s, p, + Settings.System.ACCELEROMETER_ROTATION, + SystemSettingsProto.ACCELEROMETER_ROTATION); + dumpSetting(s, p, + Settings.System.USER_ROTATION, + SystemSettingsProto.USER_ROTATION); + dumpSetting(s, p, + Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, + SystemSettingsProto.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY); + dumpSetting(s, p, + Settings.System.VIBRATE_WHEN_RINGING, + SystemSettingsProto.VIBRATE_WHEN_RINGING); + dumpSetting(s, p, + Settings.System.DTMF_TONE_WHEN_DIALING, + SystemSettingsProto.DTMF_TONE_WHEN_DIALING); + dumpSetting(s, p, + Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, + SystemSettingsProto.DTMF_TONE_TYPE_WHEN_DIALING); + dumpSetting(s, p, + Settings.System.HEARING_AID, + SystemSettingsProto.HEARING_AID); + dumpSetting(s, p, + Settings.System.TTY_MODE, + SystemSettingsProto.TTY_MODE); + dumpSetting(s, p, + Settings.System.SOUND_EFFECTS_ENABLED, + SystemSettingsProto.SOUND_EFFECTS_ENABLED); + dumpSetting(s, p, + Settings.System.HAPTIC_FEEDBACK_ENABLED, + SystemSettingsProto.HAPTIC_FEEDBACK_ENABLED); + dumpSetting(s, p, + Settings.System.NOTIFICATION_LIGHT_PULSE, + SystemSettingsProto.NOTIFICATION_LIGHT_PULSE); + dumpSetting(s, p, + Settings.System.POINTER_LOCATION, + SystemSettingsProto.POINTER_LOCATION); + dumpSetting(s, p, + Settings.System.SHOW_TOUCHES, + SystemSettingsProto.SHOW_TOUCHES); + dumpSetting(s, p, + Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, + SystemSettingsProto.WINDOW_ORIENTATION_LISTENER_LOG); + dumpSetting(s, p, + Settings.System.LOCKSCREEN_SOUNDS_ENABLED, + SystemSettingsProto.LOCKSCREEN_SOUNDS_ENABLED); + dumpSetting(s, p, + Settings.System.LOCKSCREEN_DISABLED, + SystemSettingsProto.LOCKSCREEN_DISABLED); + dumpSetting(s, p, + Settings.System.SIP_RECEIVE_CALLS, + SystemSettingsProto.SIP_RECEIVE_CALLS); + dumpSetting(s, p, + Settings.System.SIP_CALL_OPTIONS, + SystemSettingsProto.SIP_CALL_OPTIONS); + dumpSetting(s, p, + Settings.System.SIP_ALWAYS, + SystemSettingsProto.SIP_ALWAYS); + dumpSetting(s, p, + Settings.System.SIP_ADDRESS_ONLY, + SystemSettingsProto.SIP_ADDRESS_ONLY); + dumpSetting(s, p, + Settings.System.POINTER_SPEED, + SystemSettingsProto.POINTER_SPEED); + dumpSetting(s, p, + Settings.System.LOCK_TO_APP_ENABLED, + SystemSettingsProto.LOCK_TO_APP_ENABLED); + dumpSetting(s, p, + Settings.System.EGG_MODE, + SystemSettingsProto.EGG_MODE); + dumpSetting(s, p, + Settings.System.WHEN_TO_MAKE_WIFI_CALLS, + SystemSettingsProto.WHEN_TO_MAKE_WIFI_CALLS); + } +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 527631e0fb7c..697999526c23 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -17,6 +17,7 @@ package com.android.providers.settings; import android.Manifest; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.backup.BackupManager; @@ -65,6 +66,7 @@ import android.util.ByteStringUtils; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageMonitor; @@ -90,8 +92,9 @@ import java.util.Set; import java.util.regex.Pattern; import static android.os.Process.ROOT_UID; -import static android.os.Process.SYSTEM_UID; import static android.os.Process.SHELL_UID; +import static android.os.Process.SYSTEM_UID; + /** * <p> @@ -601,6 +604,22 @@ public class SettingsProvider extends ContentProvider { return cacheDir; } + /** + * Dump all settings as a proto buf. + * + * @param fd The file to dump to + */ + void dumpProto(@NonNull FileDescriptor fd) { + ProtoOutputStream proto = new ProtoOutputStream(fd); + + synchronized (mLock) { + SettingsProtoDumpUtil.dumpProtoLocked(mSettingsRegistry, proto); + + } + + proto.flush(); + } + public void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { synchronized (mLock) { final long identity = Binder.clearCallingIdentity(); @@ -663,7 +682,7 @@ public class SettingsProvider extends ContentProvider { pw.print(" value:"); pw.print(toDumpString(setting.getValue())); if (setting.getDefaultValue() != null) { pw.print(" default:"); pw.print(setting.getDefaultValue()); - pw.print(" defaultSystemSet:"); pw.print(setting.isDefaultSystemSet()); + pw.print(" defaultSystemSet:"); pw.print(setting.isDefaultFromSystem()); } if (setting.getTag() != null) { pw.print(" tag:"); pw.print(setting.getTag()); @@ -2296,7 +2315,7 @@ public class SettingsProvider extends ContentProvider { Setting setting = settingsState.getSettingLocked(name); if (!SettingsState.isSystemPackage(getContext(), setting.getPackageName())) { - if (setting.isDefaultSystemSet()) { + if (setting.isDefaultFromSystem()) { if (settingsState.resetSettingLocked(name, packageName)) { notifyForSettingsChange(key, name); } @@ -2310,7 +2329,7 @@ public class SettingsProvider extends ContentProvider { case Settings.RESET_MODE_TRUSTED_DEFAULTS: { for (String name : settingsState.getSettingNamesLocked()) { Setting setting = settingsState.getSettingLocked(name); - if (setting.isDefaultSystemSet()) { + if (setting.isDefaultFromSystem()) { if (settingsState.resetSettingLocked(name, packageName)) { notifyForSettingsChange(key, name); } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java index fecc938ad3be..2d5932492b9a 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java @@ -65,6 +65,7 @@ final public class SettingsService extends Binder { } int opti = 0; + boolean dumpAsProto = false; while (opti < args.length) { String opt = args[opti]; if (opt == null || opt.length() <= 0 || opt.charAt(0) != '-') { @@ -74,16 +75,22 @@ final public class SettingsService extends Binder { if ("-h".equals(opt)) { MyShellCommand.dumpHelp(pw, true); return; + } else if ("--proto".equals(opt)) { + dumpAsProto = true; } else { pw.println("Unknown argument: " + opt + "; use -h for help"); } } - long caller = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { - mProvider.dumpInternal(fd, pw, args); + if (dumpAsProto) { + mProvider.dumpProto(fd); + } else { + mProvider.dumpInternal(fd, pw, args); + } } finally { - Binder.restoreCallingIdentity(caller); + Binder.restoreCallingIdentity(ident); } } @@ -449,8 +456,9 @@ final public class SettingsService extends Binder { static void dumpHelp(PrintWriter pw, boolean dumping) { if (dumping) { pw.println("Settings provider dump options:"); - pw.println(" [-h]"); + pw.println(" [-h] [--proto]"); pw.println(" -h: print this help."); + pw.println(" --proto: dump as protobuf."); } else { pw.println("Settings provider (settings) commands:"); pw.println(" help"); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 8f37b98334b7..a74be35b3f28 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -16,6 +16,9 @@ package com.android.providers.settings; +import static android.os.Process.FIRST_APPLICATION_UID; + +import android.annotation.NonNull; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -30,6 +33,8 @@ import android.os.Message; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.providers.settings.GlobalSettingsProto; +import android.providers.settings.SettingsOperationProto; import android.text.TextUtils; import android.util.ArrayMap; import android.util.AtomicFile; @@ -38,10 +43,14 @@ import android.util.Slog; import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.Xml; +import android.util.proto.ProtoOutputStream; + import com.android.internal.annotations.GuardedBy; import com.android.server.LocalServices; + import libcore.io.IoUtils; import libcore.util.Objects; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; @@ -56,8 +65,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import static android.os.Process.FIRST_APPLICATION_UID; - /** * This class contains the state for one type of settings. It is responsible * for saving the state asynchronously to an XML file after a mutation and @@ -404,6 +411,38 @@ final class SettingsState { } } + /** + * Dump historical operations as a proto buf. + * + * @param proto The proto buf stream to dump to + */ + void dumpProtoHistoricalOperations(@NonNull ProtoOutputStream proto) { + synchronized (mLock) { + if (mHistoricalOperations == null) { + return; + } + + final int operationCount = mHistoricalOperations.size(); + for (int i = 0; i < operationCount; i++) { + int index = mNextHistoricalOpIdx - 1 - i; + if (index < 0) { + index = operationCount + index; + } + HistoricalOperation operation = mHistoricalOperations.get(index); + long settingsOperationToken = proto.start(GlobalSettingsProto.HISTORICAL_OP); + proto.write(SettingsOperationProto.TIMESTAMP, operation.mTimestamp); + proto.write(SettingsOperationProto.OPERATION, operation.mOperation); + if (operation.mSetting != null) { + // Only add the name of the setting, since we don't know the historical package + // and values for it so they would be misleading to add here (all we could + // add is what the current data is). + proto.write(SettingsOperationProto.SETTING, operation.mSetting.getName()); + } + proto.end(settingsOperationToken); + } + } + } + public void dumpHistoricalOperations(PrintWriter pw) { synchronized (mLock) { if (mHistoricalOperations == null) { @@ -544,7 +583,7 @@ final class SettingsState { writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(), setting.getValue(), setting.getDefaultValue(), setting.getPackageName(), - setting.getTag(), setting.isDefaultSystemSet()); + setting.getTag(), setting.isDefaultFromSystem()); if (DEBUG_PERSISTENCE) { Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue()); @@ -763,7 +802,7 @@ final class SettingsState { private String id; private String tag; // Whether the default is set by the system - private boolean defaultSystemSet; + private boolean defaultFromSystem; public Setting(Setting other) { name = other.name; @@ -771,7 +810,7 @@ final class SettingsState { defaultValue = other.defaultValue; packageName = other.packageName; id = other.id; - defaultSystemSet = other.defaultSystemSet; + defaultFromSystem = other.defaultFromSystem; tag = other.tag; } @@ -798,7 +837,7 @@ final class SettingsState { this.defaultValue = defaultValue; this.packageName = packageName; this.id = id; - this.defaultSystemSet = fromSystem; + this.defaultFromSystem = fromSystem; } public String getName() { @@ -825,8 +864,8 @@ final class SettingsState { return packageName; } - public boolean isDefaultSystemSet() { - return defaultSystemSet; + public boolean isDefaultFromSystem() { + return defaultFromSystem; } public String getId() { @@ -854,22 +893,22 @@ final class SettingsState { } String defaultValue = this.defaultValue; - boolean defaultSystemSet = this.defaultSystemSet; + boolean defaultFromSystem = this.defaultFromSystem; if (setDefault) { if (!Objects.equal(value, this.defaultValue) - && (!defaultSystemSet || callerSystem)) { + && (!defaultFromSystem || callerSystem)) { defaultValue = value; // Default null means no default, so the tag is irrelevant // since it is used to reset a settings subset their defaults. // Also it is irrelevant if the system set the canonical default. if (defaultValue == null) { tag = null; - defaultSystemSet = false; + defaultFromSystem = false; } } - if (!defaultSystemSet && value != null) { + if (!defaultFromSystem && value != null) { if (callerSystem) { - defaultSystemSet = true; + defaultFromSystem = true; } } } @@ -879,11 +918,11 @@ final class SettingsState { && Objects.equal(defaultValue, this.defaultValue) && Objects.equal(packageName, this.packageName) && Objects.equal(tag, this.tag) - && defaultSystemSet == this.defaultSystemSet) { + && defaultFromSystem == this.defaultFromSystem) { return false; } - init(name, value, tag, defaultValue, packageName, defaultSystemSet, + init(name, value, tag, defaultValue, packageName, defaultFromSystem, String.valueOf(mNextId++)); return true; } @@ -892,7 +931,7 @@ final class SettingsState { return "Setting{name=" + name + " value=" + value + (defaultValue != null ? " default=" + defaultValue : "") + " packageName=" + packageName + " tag=" + tag - + " defaultSystemSet=" + defaultSystemSet + "}"; + + " defaultFromSystem=" + defaultFromSystem + "}"; } } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index fede34d2ced3..f72d091607ac 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -131,6 +131,9 @@ <!-- Assist --> <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" /> + <!-- Doze mode temp whitelisting for notification dispatching. --> + <uses-permission android:name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST" /> + <!-- Listen for keyboard attachment / detachment --> <uses-permission android:name="android.permission.TABLET_MODE" /> diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java index a9d1fa94cf10..152dbc5e06be 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java @@ -14,17 +14,13 @@ package com.android.systemui.plugins; -import android.annotation.Nullable; import android.app.Fragment; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import android.view.LayoutInflater; public abstract class PluginFragment extends Fragment implements Plugin { - private static final String KEY_PLUGIN_PACKAGE = "plugin_package_name"; private Context mPluginContext; @Override @@ -33,45 +29,17 @@ public abstract class PluginFragment extends Fragment implements Plugin { } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - Context sysuiContext = getContext(); - Context pluginContext = recreatePluginContext(sysuiContext, savedInstanceState); - onCreate(sysuiContext, pluginContext); - } - if (mPluginContext == null) { - throw new RuntimeException("PluginFragments must call super.onCreate(" - + "Context sysuiContext, Context pluginContext)"); - } + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + return super.getLayoutInflater(savedInstanceState).cloneInContext(getContext()); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString(KEY_PLUGIN_PACKAGE, getContext().getPackageName()); - } - - private Context recreatePluginContext(Context sysuiContext, Bundle savedInstanceState) { - final String pkg = savedInstanceState.getString(KEY_PLUGIN_PACKAGE); - try { - ApplicationInfo appInfo = sysuiContext.getPackageManager().getApplicationInfo(pkg, 0); - return PluginManager.getInstance(sysuiContext).getContext(appInfo, pkg); - } catch (NameNotFoundException e) { - throw new RuntimeException("Plugin with invalid package? " + pkg, e); - } - } - - @Override - public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { - return super.getLayoutInflater(savedInstanceState).cloneInContext(mPluginContext); } - /** - * Should only be called after {@link Plugin#onCreate(Context, Context)}. - */ @Override public Context getContext() { - return mPluginContext != null ? mPluginContext : super.getContext(); + return mPluginContext; } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java index 47b97bdc29e7..9f44bd4bc331 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -177,8 +177,12 @@ public class PluginInstanceManager<T extends Plugin> { if (DEBUG) Log.d(TAG, "onPluginConnected"); PluginPrefs.setHasPlugins(mContext); PluginInfo<T> info = (PluginInfo<T>) msg.obj; - info.mPlugin.onCreate(mContext, info.mPluginContext); - mListener.onPluginConnected(info.mPlugin); + if (!(msg.obj instanceof PluginFragment)) { + // Only call onDestroy for plugins that aren't fragments, as fragments + // will get the onCreate as part of the fragment lifecycle. + info.mPlugin.onCreate(mContext, info.mPluginContext); + } + mListener.onPluginConnected(info.mPlugin, info.mPluginContext); break; case PLUGIN_DISCONNECTED: if (DEBUG) Log.d(TAG, "onPluginDisconnected"); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java index b2f92d6c017e..b488d2a84baa 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java @@ -14,6 +14,8 @@ package com.android.systemui.plugins; +import android.content.Context; + /** * Interface for listening to plugins being connected. */ @@ -24,7 +26,7 @@ public interface PluginListener<T extends Plugin> { * It may also be called in the future if the plugin package changes * and needs to be reloaded. */ - void onPluginConnected(T plugin); + void onPluginConnected(T plugin, Context pluginContext); /** * Called when a plugin has been uninstalled/updated and should be removed diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java index a9874fcf328e..e21a282581a4 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java @@ -37,7 +37,7 @@ public interface QS extends FragmentBase { // This should be incremented any time this class or ActivityStarter or BaseStatusBarHeader // change in incompatible ways. - public static final int VERSION = 4; + public static final int VERSION = 5; String TAG = "QS"; @@ -105,24 +105,8 @@ public interface QS extends FragmentBase { public abstract void setExpansion(float headerExpansionFraction); public abstract void setListening(boolean listening); public abstract void updateEverything(); - public abstract void setActivityStarter(ActivityStarter activityStarter); public abstract void setCallback(Callback qsPanelCallback); public abstract View getExpandView(); } - /** - * An interface to start activities. This is used to as a callback from the views to - * {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the - * Keyguard. - */ - public static interface ActivityStarter { - - void startPendingIntentDismissingKeyguard(PendingIntent intent); - void startActivity(Intent intent, boolean dismissShade); - void startActivity(Intent intent, boolean dismissShade, Callback callback); - - interface Callback { - void onActivityStarted(int resultCode); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarter.java b/packages/SystemUI/src/com/android/systemui/ActivityStarter.java new file mode 100644 index 000000000000..a4d8a1003f98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ActivityStarter.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import android.app.PendingIntent; +import android.content.Intent; + +/** + * An interface to start activities. This is used as a callback from the views to + * {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the + * Keyguard. + */ +public interface ActivityStarter { + + void startPendingIntentDismissingKeyguard(PendingIntent intent); + void startActivity(Intent intent, boolean dismissShade); + void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade); + void startActivity(Intent intent, boolean dismissShade, Callback callback); + void postStartActivityDismissingKeyguard(Intent intent, int delay); + void postStartActivityDismissingKeyguard(PendingIntent intent); + void postQSRunnableDismissingKeyguard(Runnable runnable); + + interface Callback { + void onActivityStarted(int resultCode); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java new file mode 100644 index 000000000000..4ae81a7017c9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import android.app.PendingIntent; +import android.content.Intent; + +/** + * Single common instance of ActivityStarter that can be gotten and referenced from anywhere, but + * delegates to an actual implementation such as PhoneStatusBar, assuming it exists. + */ +public class ActivityStarterDelegate implements ActivityStarter { + + private ActivityStarter mActualStarter; + + @Override + public void startPendingIntentDismissingKeyguard(PendingIntent intent) { + if (mActualStarter == null) return; + mActualStarter.startPendingIntentDismissingKeyguard(intent); + } + + @Override + public void startActivity(Intent intent, boolean dismissShade) { + if (mActualStarter == null) return; + mActualStarter.startActivity(intent, dismissShade); + } + + @Override + public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) { + if (mActualStarter == null) return; + mActualStarter.startActivity(intent, onlyProvisioned, dismissShade); + } + + @Override + public void startActivity(Intent intent, boolean dismissShade, Callback callback) { + if (mActualStarter == null) return; + mActualStarter.startActivity(intent, dismissShade, callback); + } + + @Override + public void postStartActivityDismissingKeyguard(Intent intent, int delay) { + if (mActualStarter == null) return; + mActualStarter.postStartActivityDismissingKeyguard(intent, delay); + } + + @Override + public void postStartActivityDismissingKeyguard(PendingIntent intent) { + if (mActualStarter == null) return; + mActualStarter.postStartActivityDismissingKeyguard(intent); + } + + @Override + public void postQSRunnableDismissingKeyguard(Runnable runnable) { + if (mActualStarter == null) return; + mActualStarter.postQSRunnableDismissingKeyguard(runnable); + } + + public void setActivityStarterImpl(ActivityStarter starter) { + mActualStarter = starter; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 030250ae3b58..b30b5968cd10 100644 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -72,6 +72,10 @@ public class BatteryMeterView extends ImageView implements @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + mBatteryController = Dependency.get(BatteryController.class); + mDrawable.setBatteryController(mBatteryController); + mBatteryController.addCallback(this); + mDrawable.startListening(); TunerService.get(getContext()).addTunable(this, StatusBarIconController.ICON_BLACKLIST); } @@ -95,13 +99,6 @@ public class BatteryMeterView extends ImageView implements } - public void setBatteryController(BatteryController mBatteryController) { - this.mBatteryController = mBatteryController; - mDrawable.setBatteryController(mBatteryController); - mBatteryController.addCallback(this); - mDrawable.startListening(); - } - public void setDarkIntensity(float f) { mDrawable.setDarkIntensity(f); } diff --git a/packages/SystemUI/src/com/android/systemui/ConfigurationChangedReceiver.java b/packages/SystemUI/src/com/android/systemui/ConfigurationChangedReceiver.java new file mode 100644 index 000000000000..4fba6404f370 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ConfigurationChangedReceiver.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import android.content.res.Configuration; + +public interface ConfigurationChangedReceiver { + void onConfigurationChanged(Configuration newConfiguration); +} diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java new file mode 100644 index 000000000000..135b12902a25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import android.content.res.Configuration; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; +import android.util.ArrayMap; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.statusbar.phone.ManagedProfileController; +import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; +import com.android.systemui.statusbar.policy.AccessibilityController; +import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryControllerImpl; +import com.android.systemui.statusbar.policy.BluetoothController; +import com.android.systemui.statusbar.policy.BluetoothControllerImpl; +import com.android.systemui.statusbar.policy.CastController; +import com.android.systemui.statusbar.policy.CastControllerImpl; +import com.android.systemui.statusbar.policy.DataSaverController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; +import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.FlashlightControllerImpl; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.policy.HotspotControllerImpl; +import com.android.systemui.statusbar.policy.KeyguardMonitor; +import com.android.systemui.statusbar.policy.KeyguardMonitorImpl; +import com.android.systemui.statusbar.policy.LocationController; +import com.android.systemui.statusbar.policy.LocationControllerImpl; +import com.android.systemui.statusbar.policy.NetworkController; +import com.android.systemui.statusbar.policy.NetworkControllerImpl; +import com.android.systemui.statusbar.policy.NextAlarmController; +import com.android.systemui.statusbar.policy.NextAlarmControllerImpl; +import com.android.systemui.statusbar.policy.RotationLockController; +import com.android.systemui.statusbar.policy.RotationLockControllerImpl; +import com.android.systemui.statusbar.policy.SecurityController; +import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.UserInfoControllerImpl; +import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.statusbar.policy.ZenModeControllerImpl; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Class to handle ugly dependencies throughout sysui until we determine the + * long-term dependency injection solution. + * + * Classes added here should be things that are expected to live the lifetime of sysui, + * and are generally applicable to many parts of sysui. They will be lazily + * initialized to ensure they aren't created on form factors that don't need them + * (e.g. HotspotController on TV). Despite being lazily initialized, it is expected + * that all dependencies will be gotten during sysui startup, and not during runtime + * to avoid jank. + * + * All classes used here are expected to manage their own lifecycle, meaning if + * they have no clients they should not have any registered resources like bound + * services, registered receivers, etc. + */ +public class Dependency extends SystemUI { + + /** + * Key for getting a background Looper for background work. + */ + public static final String BG_LOOPER = "background_loooper"; + /** + * Key for getting a Handler for receiving time tick broadcasts on. + */ + public static final String TIME_TICK_HANDLER = "time_tick_handler"; + /** + * Generic handler on the main thread. + */ + public static final String MAIN_HANDLER = "main_handler"; + + private final ArrayMap<String, Object> mDependencies = new ArrayMap<>(); + private final ArrayMap<String, DependencyProvider> mProviders = new ArrayMap<>(); + + @Override + public void start() { + sDependency = this; + // TODO: Think about ways to push these creation rules out of Dependency to cut down + // on imports. + mProviders.put(TIME_TICK_HANDLER, () -> { + HandlerThread thread = new HandlerThread("TimeTick"); + thread.start(); + return new Handler(thread.getLooper()); + }); + mProviders.put(BG_LOOPER, () -> { + HandlerThread thread = new HandlerThread("SysUiBg", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + return thread.getLooper(); + }); + mProviders.put(MAIN_HANDLER, () -> new Handler(Looper.getMainLooper())); + mProviders.put(ActivityStarter.class.getName(), () -> new ActivityStarterDelegate()); + mProviders.put(ActivityStarterDelegate.class.getName(), () -> + getDependency(ActivityStarter.class)); + + mProviders.put(BluetoothController.class.getName(), () -> + new BluetoothControllerImpl(mContext, getDependency(BG_LOOPER))); + + mProviders.put(LocationController.class.getName(), () -> + new LocationControllerImpl(mContext, getDependency(BG_LOOPER))); + + mProviders.put(RotationLockController.class.getName(), () -> + new RotationLockControllerImpl(mContext)); + + mProviders.put(NetworkController.class.getName(), () -> + new NetworkControllerImpl(mContext, getDependency(BG_LOOPER), + getDependency(DeviceProvisionedController.class))); + + mProviders.put(ZenModeController.class.getName(), () -> + new ZenModeControllerImpl(mContext, getDependency(MAIN_HANDLER))); + + mProviders.put(HotspotController.class.getName(), () -> + new HotspotControllerImpl(mContext)); + + mProviders.put(CastController.class.getName(), () -> + new CastControllerImpl(mContext)); + + mProviders.put(FlashlightController.class.getName(), () -> + new FlashlightControllerImpl(mContext)); + + mProviders.put(KeyguardMonitor.class.getName(), () -> + new KeyguardMonitorImpl(mContext)); + + mProviders.put(UserSwitcherController.class.getName(), () -> + new UserSwitcherController(mContext, getDependency(KeyguardMonitor.class), + getDependency(MAIN_HANDLER), getDependency(ActivityStarter.class))); + + mProviders.put(UserInfoController.class.getName(), () -> + new UserInfoControllerImpl(mContext)); + + mProviders.put(BatteryController.class.getName(), () -> + new BatteryControllerImpl(mContext)); + + mProviders.put(ManagedProfileController.class.getName(), () -> + new ManagedProfileControllerImpl(mContext)); + + mProviders.put(NextAlarmController.class.getName(), () -> + new NextAlarmControllerImpl(mContext)); + + mProviders.put(DataSaverController.class.getName(), () -> + get(NetworkController.class).getDataSaverController()); + + mProviders.put(AccessibilityController.class.getName(), () -> + new AccessibilityController(mContext)); + + mProviders.put(DeviceProvisionedController.class.getName(), () -> + new DeviceProvisionedControllerImpl(mContext)); + + mProviders.put(AssistManager.class.getName(), () -> + new AssistManager(getDependency(DeviceProvisionedController.class), mContext)); + + mProviders.put(SecurityController.class.getName(), () -> + new SecurityControllerImpl(mContext)); + + // Put all dependencies above here so the factory can override them if it wants. + SystemUIFactory.getInstance().injectDependencies(mProviders, mContext); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + super.dump(fd, pw, args); + pw.println("Dumping existing controllers:"); + mDependencies.values().stream().filter(obj -> obj instanceof Dumpable) + .forEach(o -> ((Dumpable) o).dump(fd, pw, args)); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDependencies.values().stream().filter(obj -> obj instanceof ConfigurationChangedReceiver) + .forEach(o -> ((ConfigurationChangedReceiver) o).onConfigurationChanged(newConfig)); + } + + protected final <T> T getDependency(Class<T> cls) { + return getDependency(cls.getName()); + } + + protected final <T> T getDependency(String cls) { + T obj = (T) mDependencies.get(cls); + if (obj == null) { + obj = createDependency(cls); + mDependencies.put(cls, obj); + } + return obj; + } + + @VisibleForTesting + protected <T> T createDependency(String cls) { + DependencyProvider<T> provider = mProviders.get(cls); + if (provider == null) { + throw new IllegalArgumentException("Unsupported dependency " + cls); + } + return provider.createDependency(); + } + + private static Dependency sDependency; + + public interface DependencyProvider<T> { + T createDependency(); + } + + public static <T> T get(Class<T> cls) { + return sDependency.getDependency(cls.getName()); + } + + public static <T> T get(String cls) { + return sDependency.getDependency(cls); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java new file mode 100644 index 000000000000..65a6844ede2d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +public interface Dumpable { + void dump(FileDescriptor fd, PrintWriter pw, String[] args); +} diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java index efa7cae0821d..efddf206878e 100644 --- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java +++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java @@ -98,7 +98,7 @@ public class PluginInflateContainer extends AutoReinflateContainer } @Override - public void onPluginConnected(ViewProvider plugin) { + public void onPluginConnected(ViewProvider plugin, Context context) { mPluginView = plugin.getView(); inflateLayout(); } diff --git a/packages/SystemUI/src/com/android/systemui/SysUiServiceProvider.java b/packages/SystemUI/src/com/android/systemui/SysUiServiceProvider.java index c4470cd84671..ff4b7cb0ac71 100644 --- a/packages/SystemUI/src/com/android/systemui/SysUiServiceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/SysUiServiceProvider.java @@ -14,10 +14,16 @@ package com.android.systemui; +import android.content.Context; + /** * The interface for getting core components of SysUI. Exists for Testability * since tests don't have SystemUIApplication as their ApplicationContext. */ public interface SysUiServiceProvider { <T> T getComponent(Class<T> interfaceType); + + public static <T> T getComponent(Context context, Class<T> interfaceType) { + return ((SysUiServiceProvider) context.getApplicationContext()).getComponent(interfaceType); + } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index bd4e3dcbbd48..f2aaec1dcfad 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -29,7 +29,6 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.util.Log; -import com.android.systemui.doze.DozeFactory; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyboard.KeyboardUI; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -64,6 +63,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv * The classes of the stuff to start. */ private final Class<?>[] SERVICES = new Class[] { + Dependency.class, FragmentService.class, TunerService.class, CommandQueue.CommandQueueStart.class, @@ -207,7 +207,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv PluginManager.getInstance(this).addPluginListener(OverlayPlugin.ACTION, new PluginListener<OverlayPlugin>() { @Override - public void onPluginConnected(OverlayPlugin plugin) { + public void onPluginConnected(OverlayPlugin plugin, Context pluginContext) { PhoneStatusBar phoneStatusBar = getComponent(PhoneStatusBar.class); if (phoneStatusBar != null) { plugin.setup(phoneStatusBar.getStatusBarWindow(), @@ -239,8 +239,4 @@ public class SystemUIApplication extends Application implements SysUiServiceProv public SystemUI[] getServices() { return mServices; } - - public static <T> T getComponent(Context context, Class<T> interfaceType) { - return ((SysUiServiceProvider) context.getApplicationContext()).getComponent(interfaceType); - } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 10328a4e32da..228996a707f1 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -18,12 +18,14 @@ package com.android.systemui; import android.content.ComponentName; import android.content.Context; +import android.util.ArrayMap; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.ViewMediatorCallback; +import com.android.systemui.Dependency.DependencyProvider; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; import com.android.systemui.keyguard.DismissCallbackRegistry; @@ -114,24 +116,14 @@ public class SystemUIFactory { } public QSTileHost createQSTileHost(Context context, PhoneStatusBar statusBar, - BluetoothController bluetooth, LocationController location, - RotationLockController rotation, NetworkController network, - ZenModeController zen, HotspotController hotspot, - CastController cast, FlashlightController flashlight, - UserSwitcherController userSwitcher, UserInfoController userInfo, - KeyguardMonitor keyguard, SecurityController security, - BatteryController battery, StatusBarIconController iconController, - NextAlarmController nextAlarmController) { - return new QSTileHost(context, statusBar, bluetooth, location, rotation, network, zen, - hotspot, cast, flashlight, userSwitcher, userInfo, keyguard, security, battery, - iconController, nextAlarmController); + StatusBarIconController iconController) { + return new QSTileHost(context, statusBar, iconController); } public <T> T createInstance(Class<T> classType) { return null; } - public AssistManager createAssistManager(BaseStatusBar bar, Context context) { - return new AssistManager(bar, context); - } + public void injectDependencies(ArrayMap<String, DependencyProvider> providers, + Context context) { } } diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 2576bb717690..09fdf5acf032 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -9,14 +9,15 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; @@ -33,14 +34,17 @@ import com.android.internal.app.AssistUtils; import com.android.internal.app.IVoiceInteractionSessionListener; import com.android.internal.app.IVoiceInteractionSessionShowCallback; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.settingslib.applications.InterestingConfigChanges; +import com.android.systemui.ConfigurationChangedReceiver; import com.android.systemui.R; -import com.android.systemui.statusbar.BaseStatusBar; +import com.android.systemui.SysUiServiceProvider; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; /** * Class to manage everything related to assist in SystemUI. */ -public class AssistManager { +public class AssistManager implements ConfigurationChangedReceiver { private static final String TAG = "AssistManager"; private static final String ASSIST_ICON_METADATA_NAME = @@ -52,9 +56,10 @@ public class AssistManager { protected final Context mContext; private final WindowManager mWindowManager; private final AssistDisclosure mAssistDisclosure; + private final InterestingConfigChanges mInterestingConfigChanges; private AssistOrbContainer mView; - private final BaseStatusBar mBar; + private final DeviceProvisionedController mDeviceProvisionedController; protected final AssistUtils mAssistUtils; private IVoiceInteractionSessionShowCallback mShowCallback = @@ -79,14 +84,16 @@ public class AssistManager { } }; - public AssistManager(BaseStatusBar bar, Context context) { + public AssistManager(DeviceProvisionedController controller, Context context) { mContext = context; - mBar = bar; + mDeviceProvisionedController = controller; mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mAssistUtils = new AssistUtils(context); mAssistDisclosure = new AssistDisclosure(context, new Handler()); registerVoiceInteractionSessionListener(); + mInterestingConfigChanges = new InterestingConfigChanges(ActivityInfo.CONFIG_ORIENTATION); + onConfigurationChanged(context.getResources().getConfiguration()); } protected void registerVoiceInteractionSessionListener() { @@ -104,7 +111,10 @@ public class AssistManager { }); } - public void onConfigurationChanged() { + public void onConfigurationChanged(Configuration newConfiguration) { + if (!mInterestingConfigChanges.applyNewConfig(mContext.getResources())) { + return; + } boolean visible = false; if (mView != null) { visible = mView.isShowing(); @@ -183,13 +193,13 @@ public class AssistManager { } private void startAssistActivity(Bundle args, @NonNull ComponentName assistComponent) { - if (!mBar.isDeviceProvisioned()) { + if (!mDeviceProvisionedController.isDeviceProvisioned()) { return; } // Close Recent Apps if needed - mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL); + SysUiServiceProvider.getComponent(mContext, CommandQueue.class).animateCollapsePanels( + CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL); boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java index 57857ccb8cc6..50506a95851c 100644 --- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java +++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java @@ -27,11 +27,13 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcelable; +import android.util.ArrayMap; import android.view.LayoutInflater; import android.view.View; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.SystemUIApplication; +import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.PluginManager; import java.io.FileDescriptor; @@ -47,6 +49,7 @@ public class FragmentHostManager { private final View mRootView; private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(); private final FragmentService mManager; + private final PluginFragmentManager mPlugins = new PluginFragmentManager(); private FragmentController mFragments; private FragmentLifecycleCallbacks mLifecycleCallbacks; @@ -163,6 +166,10 @@ public class FragmentHostManager { return mFragments.getFragmentManager(); } + PluginFragmentManager getPluginManager() { + return mPlugins; + } + public interface FragmentListener { void onFragmentViewCreated(String tag, Fragment fragment); @@ -198,6 +205,11 @@ public class FragmentHostManager { } @Override + public Fragment instantiate(Context context, String className, Bundle arguments) { + return mPlugins.instantiate(context, className, arguments); + } + + @Override public boolean onShouldSaveFragmentState(Fragment fragment) { return true; // True for now. } @@ -237,4 +249,57 @@ public class FragmentHostManager { return true; } } + + class PluginFragmentManager { + private final ArrayMap<String, Context> mPluginLookup = new ArrayMap<>(); + + public void removePlugin(String tag, String currentClass, String defaultClass) { + Fragment fragment = getFragmentManager().findFragmentByTag(tag); + mPluginLookup.remove(currentClass); + getFragmentManager().beginTransaction() + .replace(((View) fragment.getView().getParent()).getId(), + instantiate(mContext, defaultClass, null), tag) + .commit(); + reloadFragments(); + } + + public void setCurrentPlugin(String tag, String currentClass, Context context) { + Fragment fragment = getFragmentManager().findFragmentByTag(tag); + mPluginLookup.put(currentClass, context); + getFragmentManager().beginTransaction() + .replace(((View) fragment.getView().getParent()).getId(), + instantiate(context, currentClass, null), tag) + .commit(); + reloadFragments(); + } + + private void reloadFragments() { + // Save the old state. + Parcelable p = destroyFragmentHost(); + // Generate a new fragment host and restore its state. + createFragmentHost(p); + } + + Fragment instantiate(Context context, String className, Bundle arguments) { + Context pluginContext = mPluginLookup.get(className); + if (pluginContext != null) { + Fragment f = Fragment.instantiate(pluginContext, className, arguments); + if (f instanceof Plugin) { + ((Plugin) f).onCreate(mContext, pluginContext); + } + return f; + } + return Fragment.instantiate(context, className, arguments); + } + } + + private static class PluginState { + Context mContext; + String mCls; + + private PluginState(String cls, Context context) { + mCls = cls; + mContext = context; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java index e107fcd5ebae..2e6de4ac9e35 100644 --- a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java +++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java @@ -15,6 +15,7 @@ package com.android.systemui.fragments; import android.app.Fragment; +import android.content.Context; import android.util.Log; import android.view.View; @@ -30,27 +31,19 @@ public class PluginFragmentListener implements PluginListener<Plugin> { private final FragmentHostManager mFragmentHostManager; private final PluginManager mPluginManager; private final Class<? extends Fragment> mDefaultClass; - private final int mId; - private final String mTag; private final Class<? extends FragmentBase> mExpectedInterface; + private final String mTag; - public PluginFragmentListener(View view, String tag, int id, - Class<? extends Fragment> defaultFragment, + public PluginFragmentListener(View view, String tag, Class<? extends Fragment> defaultFragment, Class<? extends FragmentBase> expectedInterface) { + mTag = tag; mFragmentHostManager = FragmentHostManager.get(view); mPluginManager = PluginManager.getInstance(view.getContext()); mExpectedInterface = expectedInterface; - mTag = tag; mDefaultClass = defaultFragment; - mId = id; } public void startListening(String action, int version) { - try { - setFragment(mDefaultClass.newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e); - } mPluginManager.addPluginListener(action, this, version, false /* Only allow one */); } @@ -58,17 +51,13 @@ public class PluginFragmentListener implements PluginListener<Plugin> { mPluginManager.removePluginListener(this); } - private void setFragment(Fragment fragment) { - mFragmentHostManager.getFragmentManager().beginTransaction() - .replace(mId, fragment, mTag) - .commit(); - } - @Override - public void onPluginConnected(Plugin plugin) { + public void onPluginConnected(Plugin plugin, Context pluginContext) { try { mExpectedInterface.cast(plugin); - setFragment((Fragment) plugin); + Fragment.class.cast(plugin); + mFragmentHostManager.getPluginManager().setCurrentPlugin(mTag, + plugin.getClass().getName(), pluginContext); } catch (ClassCastException e) { Log.e(TAG, plugin.getClass().getName() + " must be a Fragment and implement " + mExpectedInterface.getName(), e); @@ -77,10 +66,7 @@ public class PluginFragmentListener implements PluginListener<Plugin> { @Override public void onPluginDisconnected(Plugin plugin) { - try { - setFragment(mDefaultClass.newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e); - } + mFragmentHostManager.getPluginManager().removePlugin(mTag, + plugin.getClass().getName(), mDefaultClass.getName()); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java index 5027144721f4..a20b7baa0261 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java @@ -33,11 +33,15 @@ import android.widget.Switch; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.SysUiServiceProvider; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader; import com.android.systemui.plugins.qs.QS.Callback; import com.android.systemui.plugins.qs.QS.DetailAdapter; +import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.QSTileHost; public class QSDetail extends LinearLayout { @@ -160,7 +164,8 @@ public class QSDetail extends LinearLayout { setupDetailHeader(adapter); if (toggleQs && !mFullyExpanded) { mTriggeredExpand = true; - mHost.animateToggleQSExpansion(); + SysUiServiceProvider.getComponent(mContext, CommandQueue.class) + .animateExpandSettingsPanel(null); } else { mTriggeredExpand = false; } @@ -171,7 +176,8 @@ public class QSDetail extends LinearLayout { x = mOpenX; y = mOpenY; if (toggleQs && mTriggeredExpand) { - mHost.animateToggleQSExpansion(); + SysUiServiceProvider.getComponent(mContext, CommandQueue.class) + .animateCollapsePanels(); mTriggeredExpand = false; } } @@ -231,12 +237,8 @@ public class QSDetail extends LinearLayout { protected void setupDetailFooter(DetailAdapter adapter) { final Intent settingsIntent = adapter.getSettingsIntent(); mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); - mDetailSettingsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mHost.startActivityDismissingKeyguard(settingsIntent); - } - }); + mDetailSettingsButton.setOnClickListener(v -> Dependency.get(ActivityStarter.class) + .postStartActivityDismissingKeyguard(settingsIntent, 0)); } protected void setupDetailHeader(final DetailAdapter adapter) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java index fb3b1d955a45..0bf3f15a5889 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java @@ -19,11 +19,9 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.UserHandle; import android.provider.Settings; import android.text.SpannableStringBuilder; import android.text.method.LinkMovementMethod; @@ -33,12 +31,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.View.OnClickListener; -import android.view.Window; import android.widget.ImageView; import android.widget.TextView; +import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.SecurityController; @@ -55,12 +54,13 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene private final ImageView mFooterIcon2; private final Context mContext; private final Callback mCallback = new Callback(); + private final SecurityController mSecurityController; + private final ActivityStarter mActivityStarter; + private final Handler mMainHandler; - private SecurityController mSecurityController; private AlertDialog mDialog; private QSTileHost mHost; protected Handler mHandler; - private final Handler mMainHandler; private boolean mIsVisible; private boolean mIsIconVisible; @@ -81,13 +81,13 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene mFooterIcon2Id = R.drawable.ic_qs_network_logging; mContext = context; mMainHandler = new Handler(Looper.getMainLooper()); + mActivityStarter = Dependency.get(ActivityStarter.class); + mSecurityController = Dependency.get(SecurityController.class); + mHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER)); } - public void setHostEnvironment(QSTileHost host, SecurityController securityController, - Looper looper) { + public void setHostEnvironment(QSTileHost host) { mHost = host; - mSecurityController = securityController; - mHandler = new H(looper); } public void setListening(boolean listening) { @@ -173,7 +173,7 @@ public class QSFooter implements OnClickListener, DialogInterface.OnClickListene public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_NEGATIVE) { final Intent settingsIntent = new Intent(ACTION_VPN_SETTINGS); - mHost.startActivityDismissingKeyguard(settingsIntent); + mActivityStarter.postStartActivityDismissingKeyguard(settingsIntent, 0); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 2de32cc6285d..e0048284f669 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -190,12 +190,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback { mHost = host; mHost.addCallback(this); setTiles(mHost.getTiles()); - mFooter.setHostEnvironment(host, host.getSecurityController(), host.getLooper()); + mFooter.setHostEnvironment(host); mCustomizePanel = customizer; if (mCustomizePanel != null) { mCustomizePanel.setHost(mHost); } - mBrightnessController.setBackgroundLooper(host.getLooper()); } public QSTileHost getHost() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index dad37b087f35..e18654e7253a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java @@ -17,7 +17,6 @@ package com.android.systemui.qs; import android.app.ActivityManager; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -32,22 +31,11 @@ import android.util.SparseArray; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.RestrictedLockUtils; +import com.android.systemui.Dependency; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSTile.State; import com.android.systemui.qs.external.TileServices; -import com.android.systemui.statusbar.phone.ManagedProfileController; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BluetoothController; -import com.android.systemui.statusbar.policy.CastController; -import com.android.systemui.statusbar.policy.FlashlightController; -import com.android.systemui.statusbar.policy.HotspotController; -import com.android.systemui.statusbar.policy.KeyguardMonitor; -import com.android.systemui.statusbar.policy.LocationController; -import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.RotationLockController; -import com.android.systemui.statusbar.policy.UserInfoController; -import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.statusbar.policy.ZenModeController; import java.util.ArrayList; import java.util.Collection; @@ -100,7 +88,7 @@ public abstract class QSTile<TState extends State> { protected QSTile(Host host) { mHost = host; mContext = host.getContext(); - mHandler = new H(host.getLooper()); + mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); } /** @@ -242,7 +230,8 @@ public abstract class QSTile<TState extends State> { protected void handleLongClick() { MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec()); - mHost.startActivityDismissingKeyguard(getLongClickIntent()); + Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( + getLongClickIntent(), 0); } public abstract Intent getLongClickIntent(); @@ -381,7 +370,8 @@ public abstract class QSTile<TState extends State> { if (mState.disabledByPolicy) { Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( mContext, mState.enforcedAdmin); - mHost.startActivityDismissingKeyguard(intent); + Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( + intent, 0); } else { mAnnounceNextStateChange = true; handleClick(); @@ -436,36 +426,17 @@ public abstract class QSTile<TState extends State> { } public interface Host { - void startActivityDismissingKeyguard(Intent intent); - void startActivityDismissingKeyguard(PendingIntent intent); - void startRunnableDismissingKeyguard(Runnable runnable); void warn(String message, Throwable t); void collapsePanels(); - void animateToggleQSExpansion(); void openPanels(); - Looper getLooper(); Context getContext(); Collection<QSTile<?>> getTiles(); void addCallback(Callback callback); void removeCallback(Callback callback); - BluetoothController getBluetoothController(); - LocationController getLocationController(); - RotationLockController getRotationLockController(); - NetworkController getNetworkController(); - ZenModeController getZenModeController(); - HotspotController getHotspotController(); - CastController getCastController(); - FlashlightController getFlashlightController(); - KeyguardMonitor getKeyguardMonitor(); - UserSwitcherController getUserSwitcherController(); - UserInfoController getUserInfoController(); - BatteryController getBatteryController(); TileServices getTileServices(); void removeTile(String tileSpec); - ManagedProfileController getManagedProfileController(); - - public interface Callback { + interface Callback { void onTilesChanged(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index 0be53b4b7c26..730b55d02017 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -36,13 +36,14 @@ import android.widget.Toolbar.OnMenuItemClickListener; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.qs.QS; import com.android.systemui.qs.QSDetailClipper; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.QSTileHost; +import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback; import java.util.ArrayList; @@ -140,7 +141,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mNotifQsContainer.setCustomizerShowing(true); announceForAccessibility(mContext.getString( R.string.accessibility_desc_quick_settings_edit)); - mHost.getKeyguardMonitor().addCallback(mKeyguardCallback); + Dependency.get(KeyguardMonitor.class).addCallback(mKeyguardCallback); } } @@ -156,7 +157,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mNotifQsContainer.setCustomizerShowing(false); announceForAccessibility(mContext.getString( R.string.accessibility_desc_quick_settings)); - mHost.getKeyguardMonitor().removeCallback(mKeyguardCallback); + Dependency.get(KeyguardMonitor.class).removeCallback(mKeyguardCallback); } } @@ -206,12 +207,9 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mTileAdapter.saveSpecs(mHost); } - private final Callback mKeyguardCallback = new Callback() { - @Override - public void onKeyguardChanged() { - if (mHost.getKeyguardMonitor().isShowing()) { - hide(0, 0); - } + private final Callback mKeyguardCallback = () -> { + if (Dependency.get(KeyguardMonitor.class).isShowing()) { + hide(0, 0); } }; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 0cd6490614bd..72e6fcc0c35f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -30,6 +30,7 @@ import android.os.Looper; import android.service.quicksettings.TileService; import android.widget.Button; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTile.DrawableIcon; @@ -59,7 +60,7 @@ public class TileQueryHelper { private void addSystemTiles(final QSTileHost host) { String possible = mContext.getString(R.string.quick_settings_tiles_stock); String[] possibleTiles = possible.split(","); - final Handler qsHandler = new Handler(host.getLooper()); + final Handler qsHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER)); final Handler mainHandler = new Handler(Looper.getMainLooper()); for (int i = 0; i < possibleTiles.length; i++) { final String spec = possibleTiles[i]; diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index cff484645d97..3afbc351582b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -39,6 +39,8 @@ import android.view.WindowManagerGlobal; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; +import com.android.systemui.Dependency; +import com.android.systemui.ActivityStarter; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener; import com.android.systemui.statusbar.phone.QSTileHost; @@ -306,13 +308,10 @@ public class CustomTile extends QSTile<QSTile.State> implements TileChangeListen } public void startUnlockAndRun() { - mHost.startRunnableDismissingKeyguard(new Runnable() { - @Override - public void run() { - try { - mService.onUnlockComplete(); - } catch (RemoteException e) { - } + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { + try { + mService.onUnlockComplete(); + } catch (RemoteException e) { } }); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 015a4c0488f3..5c23eb742270 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -30,13 +30,13 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.service.quicksettings.IQSService; -import android.service.quicksettings.IQSTileService; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import android.util.ArrayMap; import android.util.Log; import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.Dependency; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.KeyguardMonitor; @@ -284,13 +284,13 @@ public class TileServices extends IQSService.Stub { @Override public boolean isLocked() { - KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor(); + KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class); return keyguardMonitor.isShowing(); } @Override public boolean isSecure() { - KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor(); + KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class); return keyguardMonitor.isSecure() && keyguardMonitor.isShowing(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java index a82f55044bde..7e04b6754920 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java @@ -38,10 +38,12 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.BatteryInfo; import com.android.settingslib.graph.UsageView; import com.android.systemui.BatteryMeterDrawable; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.qs.external.TileColorPicker; import com.android.systemui.statusbar.policy.BatteryController; import java.text.NumberFormat; @@ -59,7 +61,7 @@ public class BatteryTile extends QSTile<QSTile.State> implements BatteryControll public BatteryTile(Host host) { super(host); - mBatteryController = host.getBatteryController(); + mBatteryController = Dependency.get(BatteryController.class); } @Override @@ -273,7 +275,7 @@ public class BatteryTile extends QSTile<QSTile.State> implements BatteryControll mDetailShown = true; v.getContext().registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_TIME_TICK), null, - PhoneStatusBar.getTimeTickHandler(v.getContext())); + Dependency.get(Dependency.TIME_TICK_HANDLER)); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 15f3c9099ac5..91e76cabbc01 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -32,7 +32,9 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; @@ -48,10 +50,12 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { private final BluetoothController mController; private final BluetoothDetailAdapter mDetailAdapter; + private final ActivityStarter mActivityStarter; public BluetoothTile(Host host) { super(host); - mController = host.getBluetoothController(); + mController = Dependency.get(BluetoothController.class); + mActivityStarter = Dependency.get(ActivityStarter.class); mDetailAdapter = (BluetoothDetailAdapter) createDetailAdapter(); } @@ -90,7 +94,8 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override protected void handleSecondaryClick() { if (!mController.canConfigBluetooth()) { - mHost.startActivityDismissingKeyguard(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)); + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0); return; } showDetail(true); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index d61e2f2bdac0..7415765ff2cc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -28,7 +28,9 @@ import android.widget.Button; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; @@ -49,12 +51,14 @@ public class CastTile extends QSTile<QSTile.BooleanState> { private final CastDetailAdapter mDetailAdapter; private final KeyguardMonitor mKeyguard; private final Callback mCallback = new Callback(); + private final ActivityStarter mActivityStarter; public CastTile(Host host) { super(host); - mController = host.getCastController(); + mController = Dependency.get(CastController.class); mDetailAdapter = new CastDetailAdapter(); - mKeyguard = host.getKeyguardMonitor(); + mKeyguard = Dependency.get(KeyguardMonitor.class); + mActivityStarter = Dependency.get(ActivityStarter.class); } @Override @@ -101,13 +105,10 @@ public class CastTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) { - mHost.startRunnableDismissingKeyguard(new Runnable() { - @Override - public void run() { - MetricsLogger.action(mContext, getMetricsCategory()); - showDetail(true); - mHost.openPanels(); - } + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { + MetricsLogger.action(mContext, getMetricsCategory()); + showDetail(true); + mHost.openPanels(); }); return; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index 5f7c803f53d7..75c4a753992d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -29,7 +29,9 @@ import android.widget.Button; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.net.DataUsageController; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSIconView; import com.android.systemui.qs.QSTile; @@ -48,10 +50,12 @@ public class CellularTile extends QSTile<QSTile.SignalState> { private final CellularDetailAdapter mDetailAdapter; private final CellSignalCallback mSignalCallback = new CellSignalCallback(); + private final ActivityStarter mActivityStarter; public CellularTile(Host host) { super(host); - mController = host.getNetworkController(); + mController = Dependency.get(NetworkController.class); + mActivityStarter = Dependency.get(ActivityStarter.class); mDataController = mController.getMobileDataController(); mDetailAdapter = new CellularDetailAdapter(); } @@ -96,7 +100,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> { if (mDataController.isMobileDataSupported()) { showDetail(true); } else { - mHost.startActivityDismissingKeyguard(CELLULAR_SETTINGS); + mActivityStarter.postStartActivityDismissingKeyguard(CELLULAR_SETTINGS, 0); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java index aadc8e74c123..412fe3dee93e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java @@ -21,11 +21,13 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.DataSaverController; +import com.android.systemui.statusbar.policy.NetworkController; public class DataSaverTile extends QSTile<QSTile.BooleanState> implements DataSaverController.Listener{ @@ -34,7 +36,7 @@ public class DataSaverTile extends QSTile<QSTile.BooleanState> implements public DataSaverTile(Host host) { super(host); - mDataSaverController = host.getNetworkController().getDataSaverController(); + mDataSaverController = Dependency.get(NetworkController.class).getDataSaverController(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index a25b7ea3d16c..3c1f50461b63 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -35,9 +35,11 @@ import android.widget.Toast; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.SysUIToast; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.ZenModeController; @@ -74,7 +76,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> { public DndTile(Host host) { super(host); - mController = host.getZenModeController(); + mController = Dependency.get(ZenModeController.class); mDetailAdapter = new DndDetailAdapter(); mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_SET_VISIBLE)); mReceiverRegistered = true; @@ -313,7 +315,8 @@ public class DndTile extends QSTile<QSTile.BooleanState> { private final ZenModePanel.Callback mZenModePanelCallback = new ZenModePanel.Callback() { @Override public void onPrioritySettings() { - mHost.startActivityDismissingKeyguard(ZEN_PRIORITY_SETTINGS); + Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( + ZEN_PRIORITY_SETTINGS, 0); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 4bbc458c949d..ac82753004b0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -27,6 +27,7 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.FlashlightController; @@ -45,14 +46,12 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements public FlashlightTile(Host host) { super(host); - mFlashlightController = host.getFlashlightController(); - mFlashlightController.addCallback(this); + mFlashlightController = Dependency.get(FlashlightController.class); } @Override protected void handleDestroy() { super.handleDestroy(); - mFlashlightController.removeCallback(this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index 9d495cebde18..70f81096bb79 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -29,6 +29,7 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.GlobalSetting; import com.android.systemui.qs.QSTile; @@ -52,7 +53,7 @@ public class HotspotTile extends QSTile<QSTile.AirplaneBooleanState> { public HotspotTile(Host host) { super(host); - mController = host.getHotspotController(); + mController = Dependency.get(HotspotController.class); mAirplaneMode = new GlobalSetting(mContext, mHandler, Global.AIRPLANE_MODE_ON) { @Override protected void handleValueChanged(int value) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java index f9688169d795..fcc959601dab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java @@ -31,6 +31,8 @@ import android.util.Log; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; +import com.android.systemui.ActivityStarter; import com.android.systemui.qs.QSTile; import java.util.Arrays; @@ -105,7 +107,7 @@ public class IntentTile extends QSTile<QSTile.State> { try { if (pi != null) { if (pi.isActivity()) { - getHost().startActivityDismissingKeyguard(pi); + Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(pi); } else { pi.send(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java index 002e2a6a68e6..5374f18d1ad9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java @@ -18,14 +18,15 @@ package com.android.systemui.qs.tiles; import android.content.Intent; import android.os.UserManager; - import android.provider.Settings; import android.service.quicksettings.Tile; import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.LocationController; @@ -47,8 +48,8 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { public LocationTile(Host host) { super(host); - mController = host.getLocationController(); - mKeyguard = host.getKeyguardMonitor(); + mController = Dependency.get(LocationController.class); + mKeyguard = Dependency.get(KeyguardMonitor.class); } @Override @@ -75,18 +76,15 @@ public class LocationTile extends QSTile<QSTile.BooleanState> { @Override protected void handleClick() { if (mKeyguard.isSecure() && mKeyguard.isShowing()) { - mHost.startRunnableDismissingKeyguard(new Runnable() { - @Override - public void run() { - final boolean wasEnabled = (Boolean) mState.value; - mHost.openPanels(); - MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); - mController.setLocationEnabled(!wasEnabled); - } + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { + final boolean wasEnabled = mState.value; + mHost.openPanels(); + MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); + mController.setLocationEnabled(!wasEnabled); }); return; } - final boolean wasEnabled = (Boolean) mState.value; + final boolean wasEnabled = mState.value; MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); mController.setLocationEnabled(!wasEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index 9be67da13493..2c0af1726506 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -26,6 +26,7 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.RotationLockController; @@ -51,7 +52,7 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> { public RotationLockTile(Host host) { super(host); - mController = host.getRotationLockController(); + mController = Dependency.get(RotationLockController.class); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java index 246c23e20f48..c20c6bb7e591 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserTile.java @@ -22,6 +22,7 @@ import android.provider.Settings; import android.util.Pair; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.UserInfoController; @@ -35,8 +36,8 @@ public class UserTile extends QSTile<QSTile.State> implements UserInfoController public UserTile(Host host) { super(host); - mUserSwitcherController = host.getUserSwitcherController(); - mUserInfoController = host.getUserInfoController(); + mUserSwitcherController = Dependency.get(UserSwitcherController.class); + mUserInfoController = Dependency.get(UserInfoController.class); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java index 7d9904176af9..54b41ac5e46d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -31,7 +31,9 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.wifi.AccessPoint; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSDetailItems; import com.android.systemui.qs.QSDetailItems.Item; @@ -55,12 +57,14 @@ public class WifiTile extends QSTile<QSTile.SignalState> { private final QSTile.SignalState mStateBeforeClick = newTileState(); protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback(); + private final ActivityStarter mActivityStarter; public WifiTile(Host host) { super(host); - mController = host.getNetworkController(); + mController = Dependency.get(NetworkController.class); mWifiController = mController.getAccessPointController(); mDetailAdapter = (WifiDetailAdapter) createDetailAdapter(); + mActivityStarter = Dependency.get(ActivityStarter.class); } @Override @@ -115,7 +119,8 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override protected void handleSecondaryClick() { if (!mWifiController.canConfigWifi()) { - mHost.startActivityDismissingKeyguard(new Intent(Settings.ACTION_WIFI_SETTINGS)); + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Settings.ACTION_WIFI_SETTINGS), 0); return; } showDetail(true); @@ -329,7 +334,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override public void onSettingsActivityTriggered(Intent settingsIntent) { - mHost.startActivityDismissingKeyguard(settingsIntent); + mActivityStarter.postStartActivityDismissingKeyguard(settingsIntent, 0); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index 207deff7de96..ae4d6c9cea99 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -23,6 +23,7 @@ import android.widget.Switch; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.phone.ManagedProfileController; @@ -41,7 +42,7 @@ public class WorkModeTile extends QSTile<QSTile.BooleanState> implements public WorkModeTile(Host host) { super(host); - mProfileController = host.getManagedProfileController(); + mProfileController = Dependency.get(ManagedProfileController.class); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java index 3059a0537b75..7825e9ede3b8 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java @@ -37,6 +37,7 @@ import android.widget.ImageView; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import java.util.ArrayList; @@ -70,7 +71,7 @@ public class BrightnessController implements ToggleSlider.Listener { private final CurrentUserTracker mUserTracker; private final IVrManager mVrManager; - private Handler mBackgroundHandler; + private final Handler mBackgroundHandler; private final BrightnessObserver mBrightnessObserver; private ArrayList<BrightnessStateChangeCallback> mChangeCallbacks = @@ -276,7 +277,7 @@ public class BrightnessController implements ToggleSlider.Listener { mContext = context; mIcon = icon; mControl = control; - mBackgroundHandler = new Handler(Looper.getMainLooper()); + mBackgroundHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER)); mUserTracker = new CurrentUserTracker(mContext) { @Override public void onUserSwitched(int newUserId) { @@ -298,10 +299,6 @@ public class BrightnessController implements ToggleSlider.Listener { mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService("vrmanager")); } - public void setBackgroundLooper(Looper backgroundLooper) { - mBackgroundHandler = new Handler(backgroundLooper); - } - public void addStateChangedCallback(BrightnessStateChangeCallback cb) { mChangeCallbacks.add(cb); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index faf143e098fd..6ac5cb8d3516 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -71,7 +71,6 @@ import android.util.SparseBooleanArray; import android.view.Display; import android.view.IWindowManager; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; @@ -92,6 +91,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardHostView.OnDismissAction; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.DejankUtils; +import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.RecentsComponent; @@ -100,11 +100,11 @@ import com.android.systemui.SystemUI; import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.Recents; import com.android.systemui.statusbar.NotificationData.Entry; -import com.android.systemui.statusbar.NotificationGuts.OnGutsClosedListener; import com.android.systemui.statusbar.notification.VisualStabilityManager; -import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.statusbar.policy.RemoteInputView; @@ -223,6 +223,7 @@ public abstract class BaseStatusBar extends SystemUI implements protected KeyguardManager mKeyguardManager; private LockPatternUtils mLockPatternUtils; + private DeviceProvisionedController mDeviceProvisionedController; // UI-specific methods @@ -237,8 +238,6 @@ public abstract class BaseStatusBar extends SystemUI implements protected Display mDisplay; - private boolean mDeviceProvisioned = false; - protected RecentsComponent mRecents; protected int mZenMode; @@ -270,7 +269,7 @@ public abstract class BaseStatusBar extends SystemUI implements @Override // NotificationData.Environment public boolean isDeviceProvisioned() { - return mDeviceProvisioned; + return mDeviceProvisionedController.isDeviceProvisioned(); } private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @@ -284,15 +283,17 @@ public abstract class BaseStatusBar extends SystemUI implements return mVrMode; } + private final DeviceProvisionedListener mDeviceProvisionedListener = + new DeviceProvisionedListener() { + @Override + public void onDeviceProvisionedChanged() { + updateNotifications(); + } + }; + protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { - final boolean provisioned = 0 != Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0); - if (provisioned != mDeviceProvisioned) { - mDeviceProvisioned = provisioned; - updateNotifications(); - } final int mode = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); setZenMode(mode); @@ -707,9 +708,8 @@ public abstract class BaseStatusBar extends SystemUI implements ServiceManager.checkService(DreamService.DREAM_SERVICE)); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, - mSettingsObserver); + mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); + mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, mSettingsObserver); @@ -2470,6 +2470,7 @@ public abstract class BaseStatusBar extends SystemUI implements } catch (RemoteException e) { // Ignore. } + mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index fed28e32b0bb..477701cb64c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -185,7 +185,14 @@ public class CommandQueue extends IStatusBar.Stub { public void animateCollapsePanels() { synchronized (mLock) { mHandler.removeMessages(MSG_COLLAPSE_PANELS); - mHandler.sendEmptyMessage(MSG_COLLAPSE_PANELS); + mHandler.obtainMessage(MSG_COLLAPSE_PANELS, 0, 0).sendToTarget(); + } + } + + public void animateCollapsePanels(int flags) { + synchronized (mLock) { + mHandler.removeMessages(MSG_COLLAPSE_PANELS); + mHandler.obtainMessage(MSG_COLLAPSE_PANELS, flags, 0).sendToTarget(); } } @@ -450,7 +457,7 @@ public class CommandQueue extends IStatusBar.Stub { break; case MSG_COLLAPSE_PANELS: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).animateCollapsePanels(0); + mCallbacks.get(i).animateCollapsePanels(msg.arg1); } break; case MSG_EXPAND_SETTINGS: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index f451aef2dc5f..08fd93da7924 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -42,10 +42,10 @@ import android.view.ViewGroup; import com.android.internal.app.IBatteryStats; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.LockIcon; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; /** @@ -109,7 +109,7 @@ public class KeyguardIndicationController { KeyguardUpdateMonitor.getInstance(context).registerCallback(mUpdateMonitor); context.registerReceiverAsUser(mTickReceiver, UserHandle.SYSTEM, new IntentFilter(Intent.ACTION_TIME_TICK), null, - PhoneStatusBar.getTimeTickHandler(mContext)); + Dependency.get(Dependency.TIME_TICK_HANDLER)); updateDisclosure(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index d77e9eda5baa..1128101f142b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -37,8 +37,10 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.LinearLayout; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; @@ -62,8 +64,8 @@ public class SignalClusterView private static final String SLOT_WIFI = "wifi"; private static final String SLOT_ETHERNET = "ethernet"; - NetworkControllerImpl mNC; - SecurityController mSC; + private final NetworkController mNetworkController; + private final SecurityController mSecurityController; private boolean mNoSimsVisible = false; private boolean mVpnVisible = false; @@ -131,6 +133,8 @@ public class SignalClusterView TypedValue typedValue = new TypedValue(); res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true); mIconScaleFactor = typedValue.getFloat(); + mNetworkController = Dependency.get(NetworkController.class); + mSecurityController = Dependency.get(SecurityController.class); } @Override @@ -151,25 +155,11 @@ public class SignalClusterView mBlockEthernet = blockEthernet; mBlockWifi = blockWifi; // Re-register to get new callbacks. - mNC.removeCallback(this); - mNC.addCallback(this); + mNetworkController.removeCallback(this); + mNetworkController.addCallback(this); } } - public void setNetworkController(NetworkControllerImpl nc) { - if (DEBUG) Log.d(TAG, "NetworkController=" + nc); - mNC = nc; - mNC.addCallback(this); - } - - public void setSecurityController(SecurityController sc) { - if (DEBUG) Log.d(TAG, "SecurityController=" + sc); - mSC = sc; - mSC.addCallback(this); - mVpnVisible = mSC.isVpnEnabled(); - mVpnIconId = currentVpnIconId(mSC.isVpnBranded()); - } - @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -213,6 +203,8 @@ public class SignalClusterView @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + mVpnVisible = mSecurityController.isVpnEnabled(); + mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded()); for (PhoneState state : mPhoneStates) { if (state.mMobileGroup.getParent() == null) { @@ -227,14 +219,16 @@ public class SignalClusterView apply(); applyIconTint(); + mNetworkController.addCallback(this); + mSecurityController.addCallback(this); } @Override protected void onDetachedFromWindow() { mMobileSignalGroup.removeAllViews(); TunerService.get(mContext).removeTunable(this); - mSC.removeCallback(this); - mNC.removeCallback(this); + mSecurityController.removeCallback(this); + mNetworkController.removeCallback(this); super.onDetachedFromWindow(); } @@ -253,8 +247,8 @@ public class SignalClusterView post(new Runnable() { @Override public void run() { - mVpnVisible = mSC.isVpnEnabled(); - mVpnIconId = currentVpnIconId(mSC.isVpnBranded()); + mVpnVisible = mSecurityController.isVpnEnabled(); + mVpnIconId = currentVpnIconId(mSecurityController.isVpnBranded()); apply(); } }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java index 1c8c317790ce..3bbda4be9fd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java @@ -30,7 +30,7 @@ import android.view.View; import android.widget.LinearLayout; import com.android.systemui.R; -import com.android.systemui.plugins.qs.QS.ActivityStarter; +import com.android.systemui.ActivityStarter; import java.net.URISyntaxException; import java.util.ArrayList; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index 7adb36d615d1..93f72a8bdf78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -24,8 +24,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.PixelFormat; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -35,15 +33,16 @@ import android.view.ViewStub; import android.view.WindowManager; import android.widget.LinearLayout; import com.android.systemui.BatteryMeterView; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.recents.Recents; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.PhoneStatusBar; -import com.android.systemui.statusbar.phone.NavigationBarGestureHelper; import com.android.systemui.statusbar.phone.PhoneStatusBarView; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.UserSwitcherController; /** * A status bar (and navigation bar) tailored for the automotive use case. @@ -105,7 +104,7 @@ public class CarStatusBar extends PhoneStatusBar implements R.dimen.status_bar_connected_device_signal_margin_end)); mConnectedDeviceSignalController = new ConnectedDeviceSignalController(mContext, - mSignalsView, mBluetoothController); + mSignalsView); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView); @@ -211,8 +210,11 @@ public class CarStatusBar extends PhoneStatusBar implements @Override protected void createUserSwitcher() { - if (mUserSwitcherController.useFullscreenUserSwitcher()) { - mFullscreenUserSwitcher = new FullscreenUserSwitcher(this, mUserSwitcherController, + UserSwitcherController userSwitcherController = + Dependency.get(UserSwitcherController.class); + if (userSwitcherController.useFullscreenUserSwitcher()) { + mFullscreenUserSwitcher = new FullscreenUserSwitcher(this, + userSwitcherController, (ViewStub) mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub)); } else { super.createUserSwitcher(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java index a3e1b3ac489f..c308930cf691 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java @@ -15,6 +15,7 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import android.widget.ImageView; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.ScalingDrawableWrapper; import com.android.systemui.statusbar.policy.BluetoothController; @@ -67,10 +68,9 @@ public class ConnectedDeviceSignalController extends BroadcastReceiver implement private BluetoothHeadsetClient mBluetoothHeadsetClient; - public ConnectedDeviceSignalController(Context context, View signalsView, - BluetoothController controller) { + public ConnectedDeviceSignalController(Context context, View signalsView) { mContext = context; - mController = controller; + mController = Dependency.get(BluetoothController.class); mSignalsView = signalsView; mNetworkSignalView = (ImageView) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index a01116239ed4..31cfa66db689 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -16,7 +16,10 @@ package com.android.systemui.statusbar.phone; import android.content.Context; import android.os.Handler; +import android.os.Looper; import android.provider.Settings.Secure; + +import com.android.systemui.Dependency; import com.android.systemui.Prefs; import com.android.systemui.Prefs.Key; import com.android.systemui.qs.SecureSetting; @@ -37,12 +40,12 @@ public class AutoTileManager { public AutoTileManager(Context context, QSTileHost host) { mContext = context; mHost = host; - mHandler = new Handler(mHost.getLooper()); + mHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER)); if (!Prefs.getBoolean(context, Key.QS_HOTSPOT_ADDED, false)) { - host.getHotspotController().addCallback(mHotspotCallback); + Dependency.get(HotspotController.class).addCallback(mHotspotCallback); } if (!Prefs.getBoolean(context, Key.QS_DATA_SAVER_ADDED, false)) { - host.getNetworkController().getDataSaverController().addCallback(mDataSaverListener); + Dependency.get(DataSaverController.class).addCallback(mDataSaverListener); } if (!Prefs.getBoolean(context, Key.QS_INVERT_COLORS_ADDED, false)) { mColorsSetting = new SecureSetting(mContext, mHandler, @@ -52,43 +55,33 @@ public class AutoTileManager { if (value != 0) { mHost.addTile("inversion"); Prefs.putBoolean(mContext, Key.QS_INVERT_COLORS_ADDED, true); - mHandler.post(new Runnable() { - @Override - public void run() { - mColorsSetting.setListening(false); - } - }); + mHandler.post(() -> mColorsSetting.setListening(false)); } } }; mColorsSetting.setListening(true); } if (!Prefs.getBoolean(context, Key.QS_WORK_ADDED, false)) { - host.getManagedProfileController().addCallback(mProfileCallback); + Dependency.get(ManagedProfileController.class).addCallback(mProfileCallback); } } public void destroy() { mColorsSetting.setListening(false); - mHost.getHotspotController().removeCallback(mHotspotCallback); - mHost.getNetworkController().getDataSaverController().removeCallback(mDataSaverListener); - mHost.getManagedProfileController().removeCallback(mProfileCallback); + Dependency.get(HotspotController.class).removeCallback(mHotspotCallback); + Dependency.get(DataSaverController.class).removeCallback(mDataSaverListener); + Dependency.get(ManagedProfileController.class).removeCallback(mProfileCallback); } private final ManagedProfileController.Callback mProfileCallback = new ManagedProfileController.Callback() { @Override public void onManagedProfileChanged() { - if (mHost.getManagedProfileController().hasActiveProfile()) { + if (Dependency.get(ManagedProfileController.class).hasActiveProfile()) { mHost.addTile("work"); Prefs.putBoolean(mContext, Key.QS_WORK_ADDED, true); - mHandler.post(new Runnable() { - @Override - public void run() { - mHost.getManagedProfileController().removeCallback( - mProfileCallback); - } - }); + mHandler.post(() -> Dependency.get(ManagedProfileController.class) + .removeCallback(mProfileCallback)); } } @@ -105,13 +98,8 @@ public class AutoTileManager { if (isDataSaving) { mHost.addTile("saver"); Prefs.putBoolean(mContext, Key.QS_DATA_SAVER_ADDED, true); - mHandler.post(new Runnable() { - @Override - public void run() { - mHost.getNetworkController().getDataSaverController().removeCallback( - mDataSaverListener); - } - }); + mHandler.post(() -> Dependency.get(DataSaverController.class).removeCallback( + mDataSaverListener)); } } }; @@ -122,12 +110,8 @@ public class AutoTileManager { if (enabled) { mHost.addTile("hotspot"); Prefs.putBoolean(mContext, Key.QS_HOTSPOT_ADDED, true); - mHandler.post(new Runnable() { - @Override - public void run() { - mHost.getHotspotController().removeCallback(mHotspotCallback); - } - }); + mHandler.post(() -> Dependency.get(HotspotController.class) + .removeCallback(mHotspotCallback)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index a2c106ae4190..79120d889c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -58,6 +58,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; +import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; @@ -66,7 +67,7 @@ import com.android.systemui.plugins.IntentButtonProvider.IntentButton; import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; -import com.android.systemui.plugins.qs.QS.ActivityStarter; +import com.android.systemui.ActivityStarter; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; @@ -230,6 +231,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mRightAffordanceView.setOnClickListener(this); mLeftAffordanceView.setOnClickListener(this); initAccessibility(); + mActivityStarter = Dependency.get(ActivityStarter.class); + mFlashlightController = Dependency.get(FlashlightController.class); + mAccessibilityController = Dependency.get(AccessibilityController.class); + mAssistManager = Dependency.get(AssistManager.class); + updateLeftAffordance(); } @Override @@ -299,20 +305,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mRightAffordanceView.setContentDescription(state.contentDescription); } - public void setActivityStarter(ActivityStarter activityStarter) { - mActivityStarter = activityStarter; - } - - public void setFlashlightController(FlashlightController flashlightController) { - mFlashlightController = flashlightController; - } - - public void setAccessibilityController(AccessibilityController accessibilityController) { - mAccessibilityController = accessibilityController; - mLockIcon.setAccessibilityController(accessibilityController); - accessibilityController.addStateChangedCallback(this); - } - public void setPhoneStatusBar(PhoneStatusBar phoneStatusBar) { mPhoneStatusBar = phoneStatusBar; updateCameraVisibility(); // in case onFinishInflate() was called too early @@ -761,11 +753,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mIndicationController = keyguardIndicationController; } - public void setAssistManager(AssistManager assistManager) { - mAssistManager = assistManager; - updateLeftAffordance(); - } - public void updateLeftAffordance() { updateLeftAffordanceIcon(); updateLeftPreview(); @@ -792,7 +779,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private final PluginListener<IntentButtonProvider> mRightListener = new PluginListener<IntentButtonProvider>() { @Override - public void onPluginConnected(IntentButtonProvider plugin) { + public void onPluginConnected(IntentButtonProvider plugin, Context pluginContext) { setRightButton(plugin.getIntentButton()); } @@ -805,7 +792,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private final PluginListener<IntentButtonProvider> mLeftListener = new PluginListener<IntentButtonProvider>() { @Override - public void onPluginConnected(IntentButtonProvider plugin) { + public void onPluginConnected(IntentButtonProvider plugin, Context pluginContext) { setLeftButton(plugin.getIntentButton()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index e4c778c867df..ff58e54ee1ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -28,13 +28,15 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; -import com.android.systemui.BatteryMeterView; +import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.qs.QSPanel; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; import com.android.systemui.statusbar.policy.UserSwitcherController; import java.text.NumberFormat; @@ -43,7 +45,7 @@ import java.text.NumberFormat; * The header group on Keyguard. */ public class KeyguardStatusBarView extends RelativeLayout - implements BatteryController.BatteryStateChangeCallback { + implements BatteryStateChangeCallback, OnUserInfoChangedListener { private boolean mBatteryCharging; private boolean mKeyguardUserSwitcherShowing; @@ -78,6 +80,7 @@ public class KeyguardStatusBarView extends RelativeLayout mCarrierLabel = (TextView) findViewById(R.id.keyguard_carrier_text); loadDimens(); updateUserSwitcher(); + mBatteryController = Dependency.get(BatteryController.class); } @Override @@ -203,23 +206,25 @@ public class KeyguardStatusBarView extends RelativeLayout mMultiUserSwitch.setKeyguardMode(keyguardSwitcherAvailable); } - public void setBatteryController(BatteryController batteryController) { - mBatteryController = batteryController; - ((BatteryMeterView) findViewById(R.id.battery)).setBatteryController(batteryController); + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + UserInfoController userInfoController = Dependency.get(UserInfoController.class); + userInfoController.addCallback(this); + mUserSwitcherController = Dependency.get(UserSwitcherController.class); + mMultiUserSwitch.setUserSwitcherController(mUserSwitcherController); + userInfoController.reloadUserInfo(); } - public void setUserSwitcherController(UserSwitcherController controller) { - mUserSwitcherController = controller; - mMultiUserSwitch.setUserSwitcherController(controller); + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + Dependency.get(UserInfoController.class).removeCallback(this); } - public void setUserInfoController(UserInfoController userInfoController) { - userInfoController.addCallback(new UserInfoController.OnUserInfoChangedListener() { - @Override - public void onUserInfoChanged(String name, Drawable picture, String userAccount) { - mMultiUserAvatar.setImageDrawable(picture); - } - }); + @Override + public void onUserInfoChanged(String name, Drawable picture, String userAccount) { + mMultiUserAvatar.setImageDrawable(picture); } public void setQSPanel(QSPanel qsp) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java index 26b0d532ae68..4535992ed5b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone; import android.graphics.Rect; import android.view.View; +import com.android.systemui.Dependency; import com.android.systemui.statusbar.policy.BatteryController; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; @@ -60,11 +61,10 @@ public class LightBarController implements BatteryController.BatteryStateChangeC private final Rect mLastFullscreenBounds = new Rect(); private final Rect mLastDockedBounds = new Rect(); - public LightBarController(StatusBarIconController statusBarIconController, - BatteryController batteryController) { + public LightBarController(StatusBarIconController statusBarIconController) { mStatusBarIconController = statusBarIconController; - mBatteryController = batteryController; - batteryController.addCallback(this); + mBatteryController = Dependency.get(BatteryController.class); + mBatteryController.addCallback(this); } public void setNavigationBar(LightBarTransitionsController navigationBar) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index fc33ace73f7f..316bd5bcaca2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -38,8 +38,8 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { private boolean mListening; private int mCurrentUser; - public ManagedProfileControllerImpl(QSTileHost host) { - mContext = host.getContext(); + public ManagedProfileControllerImpl(Context context) { + mContext = context; mUserManager = UserManager.get(mContext); mProfiles = new LinkedList<UserInfo>(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java index 4d4f9d285534..3cbac178f9ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -29,7 +29,9 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; import android.widget.FrameLayout; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.QSPanel; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; @@ -65,7 +67,7 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener public void setQsPanel(QSPanel qsPanel) { mQsPanel = qsPanel; - setUserSwitcherController(qsPanel.getHost().getUserSwitcherController()); + setUserSwitcherController(Dependency.get(UserSwitcherController.class)); } public boolean hasMultipleUsers() { @@ -134,7 +136,7 @@ public class MultiUserSwitch extends FrameLayout implements View.OnClickListener Intent intent = ContactsContract.QuickContact.composeQuickContactsIntent( getContext(), v, ContactsContract.Profile.CONTENT_URI, ContactsContract.QuickContact.MODE_LARGE, null); - mQsPanel.getHost().startActivityDismissingKeyguard(intent); + Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 3423a3c1b1b3..3c46d269750f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -64,7 +64,9 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.LatencyTracker; +import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.SysUiServiceProvider; import com.android.systemui.SystemUIApplication; import com.android.systemui.assist.AssistManager; import com.android.systemui.fragments.FragmentHostManager; @@ -124,16 +126,17 @@ public class NavigationBarFragment extends Fragment implements Callbacks { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mCommandQueue = SystemUIApplication.getComponent(getContext(), CommandQueue.class); + mCommandQueue = SysUiServiceProvider.getComponent(getContext(), CommandQueue.class); mCommandQueue.addCallbacks(this); - mPhoneStatusBar = SystemUIApplication.getComponent(getContext(), PhoneStatusBar.class); - mRecents = SystemUIApplication.getComponent(getContext(), Recents.class); - mDivider = SystemUIApplication.getComponent(getContext(), Divider.class); + mPhoneStatusBar = SysUiServiceProvider.getComponent(getContext(), PhoneStatusBar.class); + mRecents = SysUiServiceProvider.getComponent(getContext(), Recents.class); + mDivider = SysUiServiceProvider.getComponent(getContext(), Divider.class); mWindowManager = getContext().getSystemService(WindowManager.class); mAccessibilityManager = getContext().getSystemService(AccessibilityManager.class); if (savedInstanceState != null) { mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0); } + mAssistManager = Dependency.get(AssistManager.class); try { WindowManagerGlobal.getWindowManagerService() @@ -400,10 +403,6 @@ public class NavigationBarFragment extends Fragment implements Callbacks { ButtonDispatcher homeButton = mNavigationBarView.getHomeButton(); homeButton.setOnTouchListener(this::onHomeTouch); homeButton.setOnLongClickListener(this::onHomeLongClick); - - if (mAssistManager != null) { - mAssistManager.onConfigurationChanged(); - } } private boolean onHomeTouch(View v, MotionEvent event) { @@ -436,10 +435,6 @@ public class NavigationBarFragment extends Fragment implements Callbacks { } private void onVerticalChanged(boolean isVertical) { - if (mAssistManager != null) { - // TODO: Clean this up. - mAssistManager.onConfigurationChanged(); - } mPhoneStatusBar.setQsScrimEnabled(!isVertical); } @@ -562,11 +557,6 @@ public class NavigationBarFragment extends Fragment implements Callbacks { // ----- Methods that PhoneStatusBar talks to (should be minimized) ----- - public void setAssistManager(AssistManager assistManager) { - mAssistManager = assistManager; - mAssistManager.onConfigurationChanged(); - } - public void setLightBarController(LightBarController lightBarController) { mLightBarController = lightBarController; mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java index b6feb0eba058..f04a9ee71404 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java @@ -365,7 +365,7 @@ public class NavigationBarInflaterView extends FrameLayout } @Override - public void onPluginConnected(NavBarButtonProvider plugin) { + public void onPluginConnected(NavBarButtonProvider plugin, Context context) { mPlugins.add(plugin); clearViews(); inflateLayout(mCurrentLayout); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 31c78c8f911d..319f124f0554 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -765,7 +765,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } @Override - public void onPluginConnected(NavGesture plugin) { + public void onPluginConnected(NavGesture plugin, Context context) { mGestureHelper = plugin.getGestureHelper(); updateTaskSwitchHelper(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 001edb3a9ce3..80ad9d2d780a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.windowStateToString; + import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; @@ -71,13 +72,11 @@ import android.media.session.PlaybackState; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; -import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -120,8 +119,9 @@ import com.android.keyguard.KeyguardStatusView; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; -import com.android.systemui.BatteryMeterView; +import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.DemoMode; +import com.android.systemui.Dependency; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.Interpolators; @@ -130,6 +130,7 @@ import com.android.systemui.R; import com.android.systemui.SysUiServiceProvider; import com.android.systemui.SystemUIApplication; import com.android.systemui.SystemUIFactory; +import com.android.systemui.assist.AssistManager; import com.android.systemui.classifier.FalsingLog; import com.android.systemui.classifier.FalsingManager; import com.android.systemui.doze.DozeHost; @@ -138,7 +139,7 @@ import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.PluginFragmentListener; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.plugins.qs.QS.ActivityStarter; +import com.android.systemui.ActivityStarter; import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanel; @@ -168,27 +169,22 @@ import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; -import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.BatteryControllerImpl; -import com.android.systemui.statusbar.policy.BluetoothControllerImpl; import com.android.systemui.statusbar.policy.BrightnessMirrorController; -import com.android.systemui.statusbar.policy.CastControllerImpl; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.EncryptionHelper; -import com.android.systemui.statusbar.policy.FlashlightControllerImpl; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.statusbar.policy.HotspotControllerImpl; +import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.KeyguardMonitorImpl; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; -import com.android.systemui.statusbar.policy.LocationControllerImpl; import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.NetworkControllerImpl; -import com.android.systemui.statusbar.policy.NextAlarmControllerImpl; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.policy.PreviewInflater; -import com.android.systemui.statusbar.policy.RotationLockControllerImpl; -import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.SecurityController; +import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.policy.ZenModeController; @@ -305,25 +301,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, PhoneStatusBarPolicy mIconPolicy; - // These are no longer handled by the policy, because we need custom strategies for them - protected BluetoothControllerImpl mBluetoothController; - SecurityControllerImpl mSecurityController; - protected BatteryController mBatteryController; - LocationControllerImpl mLocationController; - NetworkControllerImpl mNetworkController; - HotspotControllerImpl mHotspotController; - RotationLockControllerImpl mRotationLockController; - UserInfoControllerImpl mUserInfoController; - protected ZenModeController mZenModeController; - CastControllerImpl mCastController; VolumeComponent mVolumeComponent; - KeyguardUserSwitcher mKeyguardUserSwitcher; - FlashlightControllerImpl mFlashlightController; - protected UserSwitcherController mUserSwitcherController; - NextAlarmControllerImpl mNextAlarmController; - protected KeyguardMonitorImpl mKeyguardMonitor; BrightnessMirrorController mBrightnessMirrorController; - AccessibilityController mAccessibilityController; protected FingerprintUnlockController mFingerprintUnlockController; LightBarController mLightBarController; protected LockscreenWallpaper mLockscreenWallpaper; @@ -410,23 +389,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, : null; private ScreenPinningRequest mScreenPinningRequest; - private HandlerThread mHandlerThread; - private HandlerThread mTimeTickThread; - private Handler mTimeTickHandler; // ensure quick settings is disabled until the current user makes it through the setup wizard private boolean mUserSetup = false; - private ContentObserver mUserSetupObserver = new ContentObserver(new Handler()) { + private DeviceProvisionedListener mUserSetupObserver = new DeviceProvisionedListener() { @Override - public void onChange(boolean selfChange) { - final boolean userSetup = 0 != Settings.Secure.getIntForUser( - mContext.getContentResolver(), - Settings.Secure.USER_SETUP_COMPLETE, - 0 /*default */, - mCurrentUserId); + public void onUserSetupChanged() { + final boolean userSetup = mDeviceProvisionedController.isUserSetup( + mDeviceProvisionedController.getCurrentUser()); if (MULTIUSER_DEBUG) Log.d(TAG, String.format("User setup changed: " + - "selfChange=%s userSetup=%s mUserSetup=%s", - selfChange, userSetup, mUserSetup)); + "userSetup=%s mUserSetup=%s", userSetup, mUserSetup)); if (userSetup != mUserSetup) { mUserSetup = userSetup; @@ -435,9 +407,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mKeyguardBottomArea != null) { mKeyguardBottomArea.setUserSetupComplete(mUserSetup); } - if (mNetworkController != null) { - mNetworkController.setUserSetupComplete(mUserSetup); - } + updateQsExpansionEnabled(); } if (mIconPolicy != null) { mIconPolicy.setCurrentUserSetup(mUserSetup); @@ -638,6 +608,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }; + private KeyguardUserSwitcher mKeyguardUserSwitcher; + private UserSwitcherController mUserSwitcherController; + private NetworkController mNetworkController; + private KeyguardMonitorImpl mKeyguardMonitor; + private BatteryController mBatteryController; + private DeviceProvisionedController mDeviceProvisionedController; + private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { final int N = array.size(); for (int i = 0 ; i < N; i++) { @@ -671,19 +648,20 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public void start() { + mNetworkController = Dependency.get(NetworkController.class); + mUserSwitcherController = Dependency.get(UserSwitcherController.class); + mKeyguardMonitor = (KeyguardMonitorImpl) Dependency.get(KeyguardMonitor.class); + mBatteryController = Dependency.get(BatteryController.class); + mAssistManager = Dependency.get(AssistManager.class); + mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); + mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); updateDisplaySize(); mScrimSrcModeEnabled = mContext.getResources().getBoolean( R.bool.config_status_bar_scrim_behind_use_src); - // Background thread for any controllers that need it. - mHandlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); - mHandlerThread.start(); - mTimeTickThread = new HandlerThread("TimeTick"); - mTimeTickThread.start(); - mTimeTickHandler = new Handler(mTimeTickThread.getLooper()); - DateTimeView.setReceiverHandler(mTimeTickHandler); + DateTimeView.setReceiverHandler(Dependency.get(Dependency.TIME_TICK_HANDLER)); putComponent(PhoneStatusBar.class, this); super.start(); // calls createAndAddWindows() @@ -694,9 +672,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // in session state // Lastly, call to the icon policy to install/update all the icons. - mIconPolicy = new PhoneStatusBarPolicy(mContext, mIconController, mCastController, - mHotspotController, mUserInfoController, mBluetoothController, - mRotationLockController, mNetworkController.getDataSaverController(), mNextAlarmController); + mIconPolicy = new PhoneStatusBarPolicy(mContext, mIconController); mIconPolicy.setCurrentUserSetup(mUserSetup); mSettingsObserver.onChange(false); // set up @@ -717,12 +693,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mDozeServiceHost = new DozeServiceHost(); putComponent(DozeHost.class, mDozeServiceHost); - setControllerUsers(); - notifyUserAboutHiddenNotifications(); mScreenPinningRequest = new ScreenPinningRequest(mContext); mFalsingManager = FalsingManager.getInstance(mContext); + Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(this); } protected void createIconController() { @@ -796,8 +771,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, // no window manager? good luck with that } - mAssistManager = SystemUIFactory.getInstance().createAssistManager(this, context); - // figure out which pixel-format to use for the status bar. mPixelFormat = PixelFormat.OPAQUE; @@ -828,8 +801,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, (KeyguardStatusView) mStatusBarWindow.findViewById(R.id.keyguard_status_view); mKeyguardBottomArea = (KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area); - mKeyguardBottomArea.setActivityStarter(this); - mKeyguardBottomArea.setAssistManager(mAssistManager); mKeyguardIndicationController = new KeyguardIndicationController(mContext, (ViewGroup) mStatusBarWindow.findViewById(R.id.keyguard_indication_area), mKeyguardBottomArea.getLockIcon()); @@ -840,7 +811,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, createIconController(); - mBatteryController = createBatteryController(); + // TODO: Find better place for this callback. mBatteryController.addCallback(new BatteryStateChangeCallback() { @Override public void onPowerSaveChanged(boolean isPowerSave) { @@ -856,7 +827,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }); - mLightBarController = new LightBarController(mIconController, mBatteryController); + mLightBarController = new LightBarController(mIconController); if (mNavigationBar != null) { mNavigationBar.setLightBarController(mLightBarController); } @@ -885,36 +856,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mNotificationPanel); // Other icons - mLocationController = new LocationControllerImpl(mContext, - mHandlerThread.getLooper()); // will post a notification - mNetworkController = new NetworkControllerImpl(mContext, mHandlerThread.getLooper()); - mNetworkController.setUserSetupComplete(mUserSetup); - mHotspotController = new HotspotControllerImpl(mContext); - mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper()); - mSecurityController = new SecurityControllerImpl(mContext); - if (mContext.getResources().getBoolean(R.bool.config_showRotationLock)) { - mRotationLockController = new RotationLockControllerImpl(mContext); - } - mUserInfoController = new UserInfoControllerImpl(mContext); mVolumeComponent = getComponent(VolumeComponent.class); - if (mVolumeComponent != null) { - mZenModeController = mVolumeComponent.getZenController(); - } - mCastController = new CastControllerImpl(mContext); - initSignalCluster(mStatusBarView); - initSignalCluster(mKeyguardStatusBar); initEmergencyCryptkeeperText(); - mFlashlightController = new FlashlightControllerImpl(mContext); - mKeyguardBottomArea.setFlashlightController(mFlashlightController); mKeyguardBottomArea.setPhoneStatusBar(this); mKeyguardBottomArea.setUserSetupComplete(mUserSetup); - mAccessibilityController = new AccessibilityController(mContext); - mKeyguardBottomArea.setAccessibilityController(mAccessibilityController); - mNextAlarmController = new NextAlarmControllerImpl(mContext); - mKeyguardMonitor = new KeyguardMonitorImpl(mContext); - mUserSwitcherController = createUserSwitcherController(); if (UserManager.get(mContext).isUserSwitcherEnabled()) { createUserSwitcher(); } @@ -923,15 +870,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, View container = mStatusBarWindow.findViewById(R.id.qs_frame); if (container != null) { FragmentHostManager fragmentHostManager = FragmentHostManager.get(container); - new PluginFragmentListener(container, QS.TAG, R.id.qs_frame, QSFragment.class, QS.class) + fragmentHostManager.getFragmentManager().beginTransaction() + .replace(R.id.qs_frame, new QSFragment(), QS.TAG) + .commit(); + new PluginFragmentListener(container, QS.TAG, QSFragment.class, QS.class) .startListening(QS.ACTION, QS.VERSION); final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this, - mBluetoothController, mLocationController, mRotationLockController, - mNetworkController, mZenModeController, mHotspotController, - mCastController, mFlashlightController, - mUserSwitcherController, mUserInfoController, mKeyguardMonitor, - mSecurityController, mBatteryController, mIconController, - mNextAlarmController); + mIconController); mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow); fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> { QS qs = (QS) f; @@ -941,21 +886,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mQSPanel.setBrightnessMirror(mBrightnessMirrorController); mKeyguardStatusBar.setQSPanel(mQSPanel); } - mHeader = qs.getHeader(); - initSignalCluster(mHeader); - mHeader.setActivityStarter(PhoneStatusBar.this); }); } - // User info. Trigger first load. - mKeyguardStatusBar.setUserInfoController(mUserInfoController); - mKeyguardStatusBar.setUserSwitcherController(mUserSwitcherController); - mUserInfoController.reloadUserInfo(); - - ((BatteryMeterView) mStatusBarView.findViewById(R.id.battery)).setBatteryController( - mBatteryController); - mKeyguardStatusBar.setBatteryController(mBatteryController); - mReportRejectedTouch = mStatusBarWindow.findViewById(R.id.report_rejected_touch); if (mReportRejectedTouch != null) { updateReportRejectedTouchVisibility(); @@ -1016,7 +949,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, android.Manifest.permission.DUMP, null); // listen for USER_SETUP_COMPLETE setting (per-user) - resetUserSetupObserver(); + mDeviceProvisionedController.addCallback(mUserSetupObserver); + mUserSetupObserver.onUserSetupChanged(); // disable profiling bars, since they overlap and clutter the output on app windows ThreadedRenderer.overrideProperty("disableProfileBars", "true"); @@ -1027,21 +961,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, return mStatusBarView; } - public Handler getTimeTickHandler() { - return mTimeTickHandler; - } - - public static Handler getTimeTickHandler(Context context) { - PhoneStatusBar statusBar = ((SysUiServiceProvider) context.getApplicationContext()) - .getComponent(PhoneStatusBar.class); - return statusBar != null ? statusBar.getTimeTickHandler() : - new Handler(Looper.getMainLooper()); - } - protected void createNavigationBar() { mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> { mNavigationBar = (NavigationBarFragment) fragment; - mNavigationBar.setAssistManager(mAssistManager); if (mLightBarController != null) { mNavigationBar.setLightBarController(mLightBarController); } @@ -1096,10 +1018,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, inflateEmptyShadeView(); updateEmptyShadeView(); mStatusBarKeyguardViewManager.onDensityOrFontScaleChanged(); - mUserInfoController.onDensityOrFontScaleChanged(); - if (mUserSwitcherController != null) { - mUserSwitcherController.onDensityOrFontScaleChanged(); - } + // TODO: Bring these out of PhoneStatusBar. + ((UserInfoControllerImpl) Dependency.get(UserInfoController.class)) + .onDensityOrFontScaleChanged(); + Dependency.get(UserSwitcherController.class).onDensityOrFontScaleChanged(); if (mKeyguardUserSwitcher != null) { mKeyguardUserSwitcher.onDensityOrFontScaleChanged(); } @@ -1129,8 +1051,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, R.dimen.signal_cluster_margin_start), 0, 0, 0); newCluster.setLayoutParams(layoutParams); - newCluster.setSecurityController(mSecurityController); - newCluster.setNetworkController(mNetworkController); viewParent.addView(newCluster, index); return newCluster; } @@ -1161,11 +1081,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, protected void createUserSwitcher() { mKeyguardUserSwitcher = new KeyguardUserSwitcher(mContext, (ViewStub) mStatusBarWindow.findViewById(R.id.keyguard_user_switcher), - mKeyguardStatusBar, mNotificationPanel, mUserSwitcherController); - } - - protected UserSwitcherController createUserSwitcherController() { - return new UserSwitcherController(mContext, mKeyguardMonitor, mHandler, this); + mKeyguardStatusBar, mNotificationPanel); } protected void inflateStatusBarWindow(Context context) { @@ -1173,15 +1089,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, R.layout.super_status_bar, null); } - protected void initSignalCluster(View containerView) { - SignalClusterView signalCluster = - (SignalClusterView) containerView.findViewById(R.id.signal_cluster); - if (signalCluster != null) { - signalCluster.setSecurityController(mSecurityController); - signalCluster.setNetworkController(mNetworkController); - } - } - public void clearAllNotifications() { // animate-swipe all dismissable notifications, then animate the shade closed @@ -2383,6 +2290,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } @Override + public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) { + startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade); + } + + @Override public void startActivity(Intent intent, boolean dismissShade, Callback callback) { startActivityDismissingKeyguard(intent, false, dismissShade, callback); } @@ -3229,30 +3141,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mStatusBarWindowManager != null) { mStatusBarWindowManager.dump(fd, pw, args); } - if (mNetworkController != null) { - mNetworkController.dump(fd, pw, args); - } - if (mBluetoothController != null) { - mBluetoothController.dump(fd, pw, args); - } - if (mHotspotController != null) { - mHotspotController.dump(fd, pw, args); - } - if (mCastController != null) { - mCastController.dump(fd, pw, args); - } - if (mUserSwitcherController != null) { - mUserSwitcherController.dump(fd, pw, args); - } - if (mBatteryController != null) { - mBatteryController.dump(fd, pw, args); - } - if (mNextAlarmController != null) { - mNextAlarmController.dump(fd, pw, args); - } - if (mSecurityController != null) { - mSecurityController.dump(fd, pw, args); - } + if (mHeadsUpManager != null) { mHeadsUpManager.dump(fd, pw, args); } else { @@ -3266,9 +3155,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (KeyguardUpdateMonitor.getInstance(mContext) != null) { KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args); } - if (mFlashlightController != null) { - mFlashlightController.dump(fd, pw, args); - } FalsingManager.getInstance(mContext).dump(pw); FalsingLog.dump(pw); @@ -3495,7 +3381,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateRowStates(); mScreenPinningRequest.onConfigurationChanged(); - mNetworkController.onConfigurationChanged(); } @Override @@ -3505,8 +3390,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, animateCollapsePanels(); updatePublicMode(); updateNotifications(); - resetUserSetupObserver(); - setControllerUsers(); clearCurrentMediaNotification(); setLockscreenUser(newUserId); } @@ -3517,26 +3400,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, updateMediaMetaData(true, false); } - private void setControllerUsers() { - if (mZenModeController != null) { - mZenModeController.setUserId(mCurrentUserId); - } - if (mSecurityController != null) { - mSecurityController.onUserSwitched(mCurrentUserId); - } - if (mNetworkController != null) { - mNetworkController.onUserSwitched(mCurrentUserId); - } - } - - private void resetUserSetupObserver() { - mContext.getContentResolver().unregisterContentObserver(mUserSetupObserver); - mUserSetupObserver.onChange(false); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), true, - mUserSetupObserver, mCurrentUserId); - } - /** * Reload some of our resources when the configuration changes. * @@ -3717,32 +3580,23 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, } }; + @Override public void postQSRunnableDismissingKeyguard(final Runnable runnable) { - mHandler.post(new Runnable() { - @Override - public void run() { - mLeaveOpenOnKeyguardHide = true; - executeRunnableDismissingKeyguard(runnable, null, false, false, false); - } + mHandler.post(() -> { + mLeaveOpenOnKeyguardHide = true; + executeRunnableDismissingKeyguard(runnable, null, false, false, false); }); } + @Override public void postStartActivityDismissingKeyguard(final PendingIntent intent) { - mHandler.post(new Runnable() { - @Override - public void run() { - startPendingIntentDismissingKeyguard(intent); - } - }); + mHandler.post(() -> startPendingIntentDismissingKeyguard(intent)); } + @Override public void postStartActivityDismissingKeyguard(final Intent intent, int delay) { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/); - } - }, delay); + mHandler.postDelayed(() -> + handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/), delay); } private void handleStartActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned) { @@ -3794,10 +3648,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mWindowManager.removeViewImmediate(mNavigationBarView); mNavigationBarView = null; } - if (mHandlerThread != null) { - mHandlerThread.quitSafely(); - mHandlerThread = null; - } mContext.unregisterReceiver(mBroadcastReceiver); mContext.unregisterReceiver(mDemoReceiver); mAssistManager.destroy(); @@ -3808,12 +3658,11 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, (SignalClusterView) mKeyguardStatusBar.findViewById(R.id.signal_cluster); final SignalClusterView signalClusterQs = (SignalClusterView) mHeader.findViewById(R.id.signal_cluster); - mNetworkController.removeCallback(signalCluster); - mNetworkController.removeCallback(signalClusterKeyguard); - mNetworkController.removeCallback(signalClusterQs); if (mQSPanel != null && mQSPanel.getHost() != null) { mQSPanel.getHost().destroy(); } + Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null); + mDeviceProvisionedController.removeCallback(mUserSetupObserver); } private boolean mDemoModeAllowed; @@ -4988,7 +4837,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, @Override public boolean isPowerSaveActive() { - return mBatteryController != null && mBatteryController.isPowerSave(); + return mBatteryController.isPowerSave(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 9ee1e8f03137..1044ecf7395a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -36,6 +36,7 @@ import android.util.Log; import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.TelephonyIntents; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; @@ -99,22 +100,19 @@ public class PhoneStatusBarPolicy implements Callback, RotationLockController.Ro private BluetoothController mBluetooth; - public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController, - CastController cast, HotspotController hotspot, UserInfoController userInfoController, - BluetoothController bluetooth, RotationLockController rotationLockController, - DataSaverController dataSaver, NextAlarmController nextAlarm) { + public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController) { mContext = context; mIconController = iconController; - mCast = cast; - mHotspot = hotspot; - mBluetooth = bluetooth; + mCast = Dependency.get(CastController.class); + mHotspot = Dependency.get(HotspotController.class); + mBluetooth = Dependency.get(BluetoothController.class); mBluetooth.addCallback(this); - mNextAlarm = nextAlarm; + mNextAlarm = Dependency.get(NextAlarmController.class); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - mUserInfoController = userInfoController; + mUserInfoController = Dependency.get(UserInfoController.class); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - mRotationLockController = rotationLockController; - mDataSaver = dataSaver; + mRotationLockController = Dependency.get(RotationLockController.class); + mDataSaver = Dependency.get(DataSaverController.class); mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast); mSlotHotspot = context.getString(com.android.internal.R.string.status_bar_hotspot); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java index d4cf5333dfbb..2f76cb1c0709 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java @@ -17,15 +17,11 @@ package com.android.systemui.statusbar.phone; import android.app.ActivityManager; -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -33,8 +29,8 @@ import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.Log; -import android.view.View; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.external.CustomTile; @@ -58,20 +54,6 @@ import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.qs.tiles.UserTile; import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BluetoothController; -import com.android.systemui.statusbar.policy.CastController; -import com.android.systemui.statusbar.policy.NextAlarmController; -import com.android.systemui.statusbar.policy.FlashlightController; -import com.android.systemui.statusbar.policy.HotspotController; -import com.android.systemui.statusbar.policy.KeyguardMonitor; -import com.android.systemui.statusbar.policy.LocationController; -import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.RotationLockController; -import com.android.systemui.statusbar.policy.SecurityController; -import com.android.systemui.statusbar.policy.UserInfoController; -import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -80,7 +62,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; /** Platform implementation of the quick settings tile host **/ public class QSTileHost implements QSTile.Host, Tunable { @@ -93,81 +74,31 @@ public class QSTileHost implements QSTile.Host, Tunable { private final PhoneStatusBar mStatusBar; private final LinkedHashMap<String, QSTile<?>> mTiles = new LinkedHashMap<>(); protected final ArrayList<String> mTileSpecs = new ArrayList<>(); - private final BluetoothController mBluetooth; - private final LocationController mLocation; - private final RotationLockController mRotation; - private final NetworkController mNetwork; - private final ZenModeController mZen; - private final HotspotController mHotspot; - private final CastController mCast; - private final Looper mLooper; - private final FlashlightController mFlashlight; - private final UserSwitcherController mUserSwitcherController; - private final UserInfoController mUserInfoController; - private final KeyguardMonitor mKeyguard; - private final SecurityController mSecurity; - private final BatteryController mBattery; - private final StatusBarIconController mIconController; private final TileServices mServices; private final List<Callback> mCallbacks = new ArrayList<>(); private final AutoTileManager mAutoTiles; - private final ManagedProfileController mProfileController; - private final NextAlarmController mNextAlarmController; - private final HandlerThread mHandlerThread; - private View mHeader; + private final StatusBarIconController mIconController; private int mCurrentUser; public QSTileHost(Context context, PhoneStatusBar statusBar, - BluetoothController bluetooth, LocationController location, - RotationLockController rotation, NetworkController network, - ZenModeController zen, HotspotController hotspot, - CastController cast, FlashlightController flashlight, - UserSwitcherController userSwitcher, UserInfoController userInfo, - KeyguardMonitor keyguard, SecurityController security, - BatteryController battery, StatusBarIconController iconController, - NextAlarmController nextAlarmController) { + StatusBarIconController iconController) { + mIconController = iconController; mContext = context; mStatusBar = statusBar; - mBluetooth = bluetooth; - mLocation = location; - mRotation = rotation; - mNetwork = network; - mZen = zen; - mHotspot = hotspot; - mCast = cast; - mFlashlight = flashlight; - mUserSwitcherController = userSwitcher; - mUserInfoController = userInfo; - mKeyguard = keyguard; - mSecurity = security; - mBattery = battery; - mIconController = iconController; - mNextAlarmController = nextAlarmController; - mProfileController = new ManagedProfileControllerImpl(this); - - mHandlerThread = new HandlerThread(QSTileHost.class.getSimpleName(), - Process.THREAD_PRIORITY_BACKGROUND); - mHandlerThread.start(); - mLooper = mHandlerThread.getLooper(); - mServices = new TileServices(this, mLooper); + mServices = new TileServices(this, Dependency.get(Dependency.BG_LOOPER)); TunerService.get(mContext).addTunable(this, TILES_SETTING); // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. mAutoTiles = new AutoTileManager(context, this); } - public NextAlarmController getNextAlarmController() { - return mNextAlarmController; - } - - public void setHeaderView(View view) { - mHeader = view; + public StatusBarIconController getIconController() { + return mIconController; } public void destroy() { - mHandlerThread.quitSafely(); mTiles.values().forEach(tile -> tile.destroy()); mAutoTiles.destroy(); TunerService.get(mContext).removeTunable(this); @@ -190,30 +121,10 @@ public class QSTileHost implements QSTile.Host, Tunable { } @Override - public void startActivityDismissingKeyguard(final Intent intent) { - mStatusBar.postStartActivityDismissingKeyguard(intent, 0); - } - - @Override - public void startActivityDismissingKeyguard(PendingIntent intent) { - mStatusBar.postStartActivityDismissingKeyguard(intent); - } - - @Override - public void startRunnableDismissingKeyguard(Runnable runnable) { - mStatusBar.postQSRunnableDismissingKeyguard(runnable); - } - - @Override public void warn(String message, Throwable t) { // already logged } - public void animateToggleQSExpansion() { - // TODO: Better path to animated panel expansion. - mHeader.callOnClick(); - } - @Override public void collapsePanels() { mStatusBar.postAnimateCollapsePanels(); @@ -225,91 +136,15 @@ public class QSTileHost implements QSTile.Host, Tunable { } @Override - public Looper getLooper() { - return mLooper; - } - - @Override public Context getContext() { return mContext; } - @Override - public BluetoothController getBluetoothController() { - return mBluetooth; - } - - @Override - public LocationController getLocationController() { - return mLocation; - } - - @Override - public RotationLockController getRotationLockController() { - return mRotation; - } - - @Override - public NetworkController getNetworkController() { - return mNetwork; - } - - @Override - public ZenModeController getZenModeController() { - return mZen; - } - - @Override - public HotspotController getHotspotController() { - return mHotspot; - } - - @Override - public CastController getCastController() { - return mCast; - } - - @Override - public FlashlightController getFlashlightController() { - return mFlashlight; - } - - @Override - public KeyguardMonitor getKeyguardMonitor() { - return mKeyguard; - } - - @Override - public UserSwitcherController getUserSwitcherController() { - return mUserSwitcherController; - } - - @Override - public UserInfoController getUserInfoController() { - return mUserInfoController; - } - - @Override - public BatteryController getBatteryController() { - return mBattery; - } - - public SecurityController getSecurityController() { - return mSecurity; - } public TileServices getTileServices() { return mServices; } - public StatusBarIconController getIconController() { - return mIconController; - } - - public ManagedProfileController getManagedProfileController() { - return mProfileController; - } - @Override public void onTuningChanged(String key, String newValue) { if (!TILES_SETTING.equals(key)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java index 2fa961d1bb8f..9e9380295f0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java @@ -40,10 +40,11 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.keyguard.KeyguardStatusView; import com.android.settingslib.Utils; +import com.android.systemui.ActivityStarter; import com.android.systemui.BatteryMeterView; +import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; -import com.android.systemui.plugins.qs.QS.ActivityStarter; import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader; import com.android.systemui.plugins.qs.QS.Callback; import com.android.systemui.qs.QSPanel; @@ -51,10 +52,9 @@ import com.android.systemui.qs.QuickQSPanel; import com.android.systemui.qs.TouchAnimator; import com.android.systemui.qs.TouchAnimator.Builder; import com.android.systemui.statusbar.SignalClusterView; -import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; +import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener; -import com.android.systemui.statusbar.policy.NetworkControllerImpl; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; import com.android.systemui.statusbar.policy.UserInfoController; @@ -71,6 +71,7 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements private ActivityStarter mActivityStarter; private NextAlarmController mNextAlarmController; + private UserInfoController mUserInfoController; private SettingsButton mSettingsButton; protected View mSettingsContainer; @@ -118,7 +119,8 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements mEdit = findViewById(android.R.id.edit); findViewById(android.R.id.edit).setOnClickListener(view -> - mHost.startRunnableDismissingKeyguard(() -> mQsPanel.showEdit(view))); + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> + mQsPanel.showEdit(view))); mDateTimeAlarmGroup = (ViewGroup) findViewById(R.id.date_time_alarm_group); mDateTimeAlarmGroup.findViewById(R.id.empty_time_view).setVisibility(View.GONE); @@ -151,6 +153,15 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements ((RippleDrawable) mExpandIndicator.getBackground()).setForceSoftware(true); updateResources(); + + // Set the light/dark theming on the header status UI to match the current theme. + SignalClusterView cluster = (SignalClusterView) findViewById(R.id.signal_cluster); + int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground); + float intensity = colorForeground / (float) Color.WHITE; + cluster.setIconTint(colorForeground, intensity, new Rect(0, 0, 0, 0)); + BatteryMeterView battery = (BatteryMeterView) findViewById(R.id.battery); + int colorSecondary = Utils.getColorAttr(getContext(), android.R.attr.textColorSecondary); + battery.setRawColors(colorForeground, colorSecondary); } @Override @@ -248,11 +259,18 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mNextAlarmController = Dependency.get(NextAlarmController.class); + mUserInfoController = Dependency.get(UserInfoController.class); + mActivityStarter = Dependency.get(ActivityStarter.class); + } + + @Override @VisibleForTesting public void onDetachedFromWindow() { setListening(false); - mHost.getUserInfoController().removeCallback(this); - mHost.getNetworkController().removeEmergencyListener(this); super.onDetachedFromWindow(); } @@ -304,16 +322,17 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements private void updateListeners() { if (mListening) { mNextAlarmController.addCallback(this); + mUserInfoController.addCallback(this); + if (Dependency.get(NetworkController.class).hasVoiceCallingFeature()) { + Dependency.get(NetworkController.class).addEmergencyListener(this); + } } else { mNextAlarmController.removeCallback(this); + mUserInfoController.removeCallback(this); + Dependency.get(NetworkController.class).removeEmergencyListener(this); } } - @Override - public void setActivityStarter(ActivityStarter activityStarter) { - mActivityStarter = activityStarter; - } - public void setQSPanel(final QSPanel qsPanel) { mQsPanel = qsPanel; setupHost(qsPanel.getHost()); @@ -324,30 +343,9 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements public void setupHost(final QSTileHost host) { mHost = host; - host.setHeaderView(mExpandIndicator); + //host.setHeaderView(mExpandIndicator); mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this); mHeaderQsPanel.setHost(host, null /* No customization in header */); - setUserInfoController(host.getUserInfoController()); - setBatteryController(host.getBatteryController()); - setNextAlarmController(host.getNextAlarmController()); - - final boolean isAPhone = mHost.getNetworkController().hasVoiceCallingFeature(); - if (isAPhone) { - mHost.getNetworkController().addEmergencyListener(this); - } - - // Set the light/dark theming on the header status UI to match the current theme. - SignalClusterView cluster = (SignalClusterView) findViewById(R.id.signal_cluster); - cluster.setNetworkController((NetworkControllerImpl) host.getNetworkController()); - cluster.setSecurityController(host.getSecurityController()); - int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground); - float intensity = colorForeground / (float) Color.WHITE; - cluster.setIconTint(colorForeground, intensity, - new Rect(0, 0, 0, 0)); - BatteryMeterView battery = (BatteryMeterView) findViewById(R.id.battery); - battery.setBatteryController(host.getBatteryController()); - int colorSecondary = Utils.getColorAttr(getContext(), android.R.attr.textColorSecondary); - battery.setRawColors(colorForeground, colorSecondary); } @Override @@ -357,7 +355,7 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); if (mSettingsButton.isTunerClick()) { - mHost.startRunnableDismissingKeyguard(() -> post(() -> { + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { if (TunerService.isTunerEnabled(mContext)) { TunerService.showResetRequest(mContext, () -> { // Relaunch settings so that the tuner disappears. @@ -370,7 +368,7 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements } startSettingsActivity(); - })); + }); } else { startSettingsActivity(); } @@ -385,18 +383,6 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements true /* dismissShade */); } - public void setNextAlarmController(NextAlarmController nextAlarmController) { - mNextAlarmController = nextAlarmController; - } - - public void setBatteryController(BatteryController batteryController) { - batteryController.addCallback(this); - } - - public void setUserInfoController(UserInfoController userInfoController) { - userInfoController.addCallback(this); - } - @Override public void setCallback(Callback qsPanelCallback) { mHeaderQsPanel.setCallback(qsPanelCallback); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index 19dcf03d470c..48ff1c15252f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -17,12 +17,13 @@ package com.android.systemui.statusbar.policy; import com.android.systemui.DemoMode; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import java.io.FileDescriptor; import java.io.PrintWriter; -public interface BatteryController extends DemoMode, +public interface BatteryController extends DemoMode, Dumpable, CallbackController<BatteryStateChangeCallback> { /** * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java index 4c1c378b4f45..df30e20ca582 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java @@ -17,11 +17,12 @@ package com.android.systemui.statusbar.policy; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.BluetoothController.Callback; import java.util.Collection; -public interface BluetoothController extends CallbackController<Callback> { +public interface BluetoothController extends CallbackController<Callback>, Dumpable { boolean isBluetoothSupported(); boolean isBluetoothEnabled(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java index 3142ddfe9fb7..e7056a6367a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java @@ -47,7 +47,7 @@ public class CallbackHandler extends Handler implements EmergencyListener, Signa private final ArrayList<SignalCallback> mSignalCallbacks = new ArrayList<>(); public CallbackHandler() { - super(); + super(Looper.getMainLooper()); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java index 6988af7d0ed3..97be6ed10bf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.policy; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.CastController.Callback; import java.util.Set; -public interface CastController extends CallbackController<Callback> { +public interface CastController extends CallbackController<Callback>, Dumpable { void setDiscovering(boolean request); void setCurrentUserId(int currentUserId); Set<CastDevice> getCastDevices(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 1dbc6647d65e..9cc97492e22c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -38,8 +38,8 @@ import android.view.Display; import android.widget.TextView; import com.android.systemui.DemoMode; +import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -107,7 +107,7 @@ public class Clock extends TextView implements DemoMode, Tunable { filter.addAction(Intent.ACTION_USER_SWITCHED); getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, - null, PhoneStatusBar.getTimeTickHandler(getContext())); + null, Dependency.get(Dependency.TIME_TICK_HANDLER)); TunerService.get(getContext()).addTunable(this, CLOCK_SECONDS, StatusBarIconController.ICON_BLACKLIST); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java index 5544c707485d..dc33633dde53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DateView.java @@ -23,12 +23,11 @@ import android.content.IntentFilter; import android.content.res.TypedArray; import android.icu.text.DateFormat; import android.icu.text.DisplayContext; -import android.os.Handler; import android.util.AttributeSet; import android.widget.TextView; +import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import java.util.Date; import java.util.Locale; @@ -87,7 +86,7 @@ public class DateView extends TextView { filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(Intent.ACTION_LOCALE_CHANGED); getContext().registerReceiver(mIntentReceiver, filter, null, - PhoneStatusBar.getTimeTickHandler(getContext())); + Dependency.get(Dependency.TIME_TICK_HANDLER)); updateClock(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java new file mode 100644 index 000000000000..aa4eaa7acf7d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.content.Context; + +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; + +public interface DeviceProvisionedController extends CallbackController<DeviceProvisionedListener> { + + boolean isDeviceProvisioned(); + boolean isUserSetup(int currentUser); + int getCurrentUser(); + + interface DeviceProvisionedListener { + default void onDeviceProvisionedChanged() { } + default void onUserSwitched() { } + default void onUserSetupChanged() { } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java new file mode 100644 index 000000000000..0fc300d1aa07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings.Global; +import android.provider.Settings.Secure; + +import com.android.systemui.Dependency; +import com.android.systemui.settings.CurrentUserTracker; + +import java.util.ArrayList; + +public class DeviceProvisionedControllerImpl extends CurrentUserTracker implements + DeviceProvisionedController { + + private final ArrayList<DeviceProvisionedListener> mListeners = new ArrayList<>(); + private final ContentResolver mContentResolver; + private final Context mContext; + private final Uri mDeviceProvisionedUri; + private final Uri mUserSetupUri; + + public DeviceProvisionedControllerImpl(Context context) { + super(context); + mContext = context; + mContentResolver = context.getContentResolver(); + mDeviceProvisionedUri = Global.getUriFor(Global.DEVICE_PROVISIONED); + mUserSetupUri = Secure.getUriFor(Secure.USER_SETUP_COMPLETE); + } + + @Override + public boolean isDeviceProvisioned() { + return Global.getInt(mContentResolver, Global.DEVICE_PROVISIONED, 0) != 0; + } + + @Override + public boolean isUserSetup(int currentUser) { + return Secure.getIntForUser(mContentResolver, Secure.USER_SETUP_COMPLETE, 0, currentUser) + != 0; + } + + @Override + public int getCurrentUser() { + return ActivityManager.getCurrentUser(); + } + + @Override + public void addCallback(DeviceProvisionedListener listener) { + mListeners.add(listener); + if (mListeners.size() == 1) { + startListening(getCurrentUser()); + } + } + + @Override + public void removeCallback(DeviceProvisionedListener listener) { + mListeners.remove(listener); + if (mListeners.size() == 0) { + stopListening(); + } + } + + private void startListening(int user) { + mContentResolver.registerContentObserver(mDeviceProvisionedUri, true, + mSettingsObserver, 0); + mContentResolver.registerContentObserver(mUserSetupUri, true, + mSettingsObserver, user); + startTracking(); + } + + private void stopListening() { + stopTracking(); + mContentResolver.unregisterContentObserver(mSettingsObserver); + } + + @Override + public void onUserSwitched(int newUserId) { + stopListening(); + startListening(newUserId); + notifyUserChanged(); + notifyUserChanged(); + } + + private void notifyUserChanged() { + mListeners.forEach(c -> c.onUserSwitched()); + } + + private void notifySetupChanged() { + mListeners.forEach(c -> c.onUserSetupChanged()); + } + + private void notifyProvisionedChanged() { + mListeners.forEach(c -> c.onDeviceProvisionedChanged()); + } + + protected final ContentObserver mSettingsObserver = new ContentObserver(Dependency.get( + Dependency.MAIN_HANDLER)) { + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + if (mUserSetupUri.equals(uri)) { + notifySetupChanged(); + } else { + notifyProvisionedChanged(); + } + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java index 6023f3ee17c0..e576f36d573a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -14,9 +14,10 @@ package com.android.systemui.statusbar.policy; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener; -public interface FlashlightController extends CallbackController<FlashlightListener> { +public interface FlashlightController extends CallbackController<FlashlightListener>, Dumpable { boolean hasFlashlight(); void setFlashlight(boolean newState); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java index daf9d6b2e88d..0543678814fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotController.java @@ -16,9 +16,10 @@ package com.android.systemui.statusbar.policy; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.HotspotController.Callback; -public interface HotspotController extends CallbackController<Callback> { +public interface HotspotController extends CallbackController<Callback>, Dumpable { boolean isHotspotEnabled(); void setHotspotEnabled(boolean enabled); boolean isHotspotSupported(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java index 1cf405020298..4b283fedfe09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java @@ -30,6 +30,7 @@ import android.view.ViewStub; import android.widget.FrameLayout; import com.android.settingslib.animation.AppearAnimationUtils; +import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.qs.tiles.UserDetailItemView; @@ -56,10 +57,10 @@ public class KeyguardUserSwitcher { private boolean mAnimating; public KeyguardUserSwitcher(Context context, ViewStub userSwitcher, - KeyguardStatusBarView statusBarView, NotificationPanelView panelView, - UserSwitcherController userSwitcherController) { + KeyguardStatusBarView statusBarView, NotificationPanelView panelView) { boolean keyguardUserSwitcherEnabled = context.getResources().getBoolean(R.bool.config_keyguardUserSwitcher) || ALWAYS_ON; + UserSwitcherController userSwitcherController = Dependency.get(UserSwitcherController.class); if (userSwitcherController != null && keyguardUserSwitcherEnabled) { mUserSwitcherContainer = (Container) userSwitcher.inflate(); mBackground = new KeyguardUserSwitcherScrim(context); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java index 082fe82a2fb9..a22fc6b8b72d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -21,17 +21,18 @@ import android.content.Intent; import android.telephony.SubscriptionInfo; import com.android.settingslib.net.DataUsageController; import com.android.settingslib.wifi.AccessPoint; +import com.android.systemui.DemoMode; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; import java.util.List; -public interface NetworkController extends CallbackController<SignalCallback> { +public interface NetworkController extends CallbackController<SignalCallback>, DemoMode { boolean hasMobileDataFeature(); void addCallback(SignalCallback cb); void removeCallback(SignalCallback cb); void setWifiEnabled(boolean enabled); - void onUserSwitched(int newUserId); AccessPointController getAccessPointController(); DataUsageController getMobileDataController(); DataSaverController getDataSaverController(); @@ -40,6 +41,9 @@ public interface NetworkController extends CallbackController<SignalCallback> { void addEmergencyListener(EmergencyListener listener); void removeEmergencyListener(EmergencyListener listener); + void setUserSetupComplete(boolean userSetup); + boolean hasEmergencyCryptKeeperText(); + boolean isRadioOn(); public interface SignalCallback { default void setWifiIndicators(boolean enabled, IconState statusIcon, IconState qsIcon, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index a7fab41dd287..edf2c8a93650 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -20,6 +20,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; @@ -42,8 +43,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.settingslib.net.DataUsageController; +import com.android.systemui.ConfigurationChangedReceiver; import com.android.systemui.DemoMode; +import com.android.systemui.Dumpable; import com.android.systemui.R; +import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -60,7 +65,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; /** Platform implementation of the network controller. **/ public class NetworkControllerImpl extends BroadcastReceiver - implements NetworkController, DemoMode, DataUsageController.NetworkNameProvider { + implements NetworkController, DemoMode, DataUsageController.NetworkNameProvider, + ConfigurationChangedReceiver, Dumpable { // debug static final String TAG = "NetworkController"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -80,6 +86,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private final boolean mHasMobileDataFeature; private final SubscriptionDefaults mSubDefaults; private final DataSaverController mDataSaverController; + private final CurrentUserTracker mUserTracker; private Config mConfig; // Subcontrollers. @@ -135,7 +142,8 @@ public class NetworkControllerImpl extends BroadcastReceiver /** * Construct this controller object and register for updates. */ - public NetworkControllerImpl(Context context, Looper bgLooper) { + public NetworkControllerImpl(Context context, Looper bgLooper, + DeviceProvisionedController deviceProvisionedController) { this(context, (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE), (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), (WifiManager) context.getSystemService(Context.WIFI_SERVICE), @@ -143,7 +151,8 @@ public class NetworkControllerImpl extends BroadcastReceiver new CallbackHandler(), new AccessPointControllerImpl(context, bgLooper), new DataUsageController(context), - new SubscriptionDefaults()); + new SubscriptionDefaults(), + deviceProvisionedController); mReceiverHandler.post(mRegisterListeners); } @@ -154,7 +163,8 @@ public class NetworkControllerImpl extends BroadcastReceiver CallbackHandler callbackHandler, AccessPointControllerImpl accessPointController, DataUsageController dataUsageController, - SubscriptionDefaults defaultsHandler) { + SubscriptionDefaults defaultsHandler, + DeviceProvisionedController deviceProvisionedController) { mContext = context; mConfig = config; mReceiverHandler = new Handler(bgLooper); @@ -191,6 +201,20 @@ public class NetworkControllerImpl extends BroadcastReceiver // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); + mUserTracker = new CurrentUserTracker(mContext) { + @Override + public void onUserSwitched(int newUserId) { + NetworkControllerImpl.this.onUserSwitched(newUserId); + } + }; + mUserTracker.startTracking(); + deviceProvisionedController.addCallback(new DeviceProvisionedListener() { + @Override + public void onUserSetupChanged() { + setUserSetupComplete(deviceProvisionedController.isUserSetup( + deviceProvisionedController.getCurrentUser())); + } + }); } public DataSaverController getDataSaverController() { @@ -358,8 +382,7 @@ public class NetworkControllerImpl extends BroadcastReceiver }.execute(); } - @Override - public void onUserSwitched(int newUserId) { + private void onUserSwitched(int newUserId) { mCurrentUserId = newUserId; mAccessPoints.onUserSwitched(newUserId); updateConnectivity(); @@ -413,7 +436,7 @@ public class NetworkControllerImpl extends BroadcastReceiver } } - public void onConfigurationChanged() { + public void onConfigurationChanged(Configuration newConfig) { mConfig = Config.readConfig(mContext); mReceiverHandler.post(new Runnable() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java index e5b0c036b326..366a752dc880 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmController.java @@ -16,9 +16,10 @@ package com.android.systemui.statusbar.policy; import android.app.AlarmManager; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; -public interface NextAlarmController extends CallbackController<NextAlarmChangeCallback> { +public interface NextAlarmController extends CallbackController<NextAlarmChangeCallback>, Dumpable { public interface NextAlarmChangeCallback { void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java index 3142228551b6..3f8e41ae39d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java @@ -15,9 +15,11 @@ */ package com.android.systemui.statusbar.policy; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback; -public interface SecurityController extends CallbackController<SecurityControllerCallback> { +public interface SecurityController extends CallbackController<SecurityControllerCallback>, + Dumpable { /** Whether the device has device owner, even if not on this user. */ boolean isDeviceManaged(); boolean hasProfileOwner(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index df959bd722ab..19ced235dc1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -39,12 +39,13 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.systemui.R; +import com.android.systemui.settings.CurrentUserTracker; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -public class SecurityControllerImpl implements SecurityController { +public class SecurityControllerImpl extends CurrentUserTracker implements SecurityController { private static final String TAG = "SecurityController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -73,6 +74,7 @@ public class SecurityControllerImpl implements SecurityController { private int mVpnUserId; public SecurityControllerImpl(Context context) { + super(context); mContext = context; mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); @@ -87,6 +89,7 @@ public class SecurityControllerImpl implements SecurityController { // TODO: re-register network callback on user change. mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); onUserSwitched(ActivityManager.getCurrentUser()); + startTracking(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 4785ba971075..f71c5d1dc41c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -52,13 +52,14 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.util.UserIcons; import com.android.settingslib.RestrictedLockUtils; +import com.android.systemui.Dependency; import com.android.systemui.GuestResumeSessionReceiver; import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.plugins.qs.QS.DetailAdapter; import com.android.systemui.qs.tiles.UserDetailView; -import com.android.systemui.plugins.qs.QS.ActivityStarter; +import com.android.systemui.ActivityStarter; import com.android.systemui.statusbar.phone.SystemUIDialog; import java.io.FileDescriptor; @@ -645,11 +646,6 @@ public class UserSwitcherController { } @VisibleForTesting - public KeyguardMonitor getKeyguardMonitor() { - return mKeyguardMonitor; - } - - @VisibleForTesting public ArrayList<UserRecord> getUsers() { return mUsers; } @@ -657,17 +653,19 @@ public class UserSwitcherController { public static abstract class BaseUserAdapter extends BaseAdapter { final UserSwitcherController mController; + private final KeyguardMonitor mKeyguardMonitor; protected BaseUserAdapter(UserSwitcherController controller) { mController = controller; + mKeyguardMonitor = Dependency.get(KeyguardMonitor.class); controller.addAdapter(new WeakReference<>(this)); } @Override public int getCount() { - boolean secureKeyguardShowing = mController.getKeyguardMonitor().isShowing() - && mController.getKeyguardMonitor().isSecure() - && !mController.getKeyguardMonitor().canSkipBouncer(); + boolean secureKeyguardShowing = mKeyguardMonitor.isShowing() + && mKeyguardMonitor.isSecure() + && !mKeyguardMonitor.canSkipBouncer(); if (!secureKeyguardShowing) { return mController.getUsers().size(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java index bcdb62dcbd78..f195f7a89f8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java @@ -30,7 +30,6 @@ public interface ZenModeController extends CallbackController<Callback> { ZenRule getManualRule(); ZenModeConfig getConfig(); long getNextAlarm(); - void setUserId(int userId); boolean isZenAvailable(); ComponentName getEffectsSuppressor(); boolean isCountdownConditionSupported(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 96efea116029..e80d3b3a6409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -40,13 +40,14 @@ import android.util.Log; import android.util.Slog; import com.android.systemui.qs.GlobalSetting; +import com.android.systemui.settings.CurrentUserTracker; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Objects; /** Platform implementation of the zen mode controller. **/ -public class ZenModeControllerImpl implements ZenModeController { +public class ZenModeControllerImpl extends CurrentUserTracker implements ZenModeController { private static final String TAG = "ZenModeController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -66,6 +67,7 @@ public class ZenModeControllerImpl implements ZenModeController { private ZenModeConfig mConfig; public ZenModeControllerImpl(Context context, Handler handler) { + super(context); mContext = context; mModeSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) { @Override @@ -87,6 +89,7 @@ public class ZenModeControllerImpl implements ZenModeController { mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); mUserManager = context.getSystemService(UserManager.class); + startTracking(); } @Override @@ -137,7 +140,7 @@ public class ZenModeControllerImpl implements ZenModeController { } @Override - public void setUserId(int userId) { + public void onUserSwitched(int userId) { mUserId = userId; if (mRegistered) { mContext.unregisterReceiver(mReceiver); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java index 1f0ee570d12f..36c673c7106b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java @@ -25,7 +25,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; public interface VolumeComponent extends DemoMode { - ZenModeController getZenController(); void dismissNow(); void onConfigurationChanged(Configuration newConfig); void dump(FileDescriptor fd, PrintWriter pw, String[] args); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java index f195a0ba2502..137a12fbdfa3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -16,20 +16,17 @@ package com.android.systemui.volume; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.media.AudioManager; import android.media.VolumePolicy; import android.os.Bundle; import android.os.Handler; -import android.provider.Settings; -import android.util.Log; import android.view.WindowManager; -import com.android.systemui.R; +import com.android.systemui.ActivityStarter; +import com.android.systemui.Dependency; import com.android.systemui.SystemUI; import com.android.systemui.SystemUIFactory; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -37,11 +34,9 @@ import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; -import com.android.systemui.volume.car.CarVolumeDialogController; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.lang.reflect.Constructor; /** * Implementation of VolumeComponent backed by the new volume dialog. @@ -69,15 +64,14 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna 400 // vibrateToSilentDebounce ); - public VolumeDialogComponent(SystemUI sysui, Context context, Handler handler, - ZenModeController zen) { + public VolumeDialogComponent(SystemUI sysui, Context context, Handler handler) { mSysui = sysui; mContext = context; mController = SystemUIFactory.getInstance().createVolumeDialogController(context, null); mController.setUserActivityListener(this); - mZenModeController = zen; + mZenModeController = Dependency.get(ZenModeController.class); mDialog = new VolumeDialog(context, WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY, - mController, zen, mVolumeDialogCallback); + mController, mZenModeController, mVolumeDialogCallback); applyConfiguration(); TunerService.get(mContext).addTunable(this, VOLUME_DOWN_SILENT, VOLUME_UP_SILENT, VOLUME_SILENT_DO_NOT_DISTURB); @@ -134,11 +128,6 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna } @Override - public ZenModeController getZenController() { - return mZenModeController; - } - - @Override public void onConfigurationChanged(Configuration newConfig) { // noop } @@ -166,7 +155,7 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna } private void startSettings(Intent intent) { - mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard(intent, + Dependency.get(ActivityStarter.class).startActivity(intent, true /* onlyProvisioned */, true /* dismissShade */); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 73d9ea7022e4..02969e483e97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -42,8 +42,7 @@ public class VolumeUI extends SystemUI { public void start() { mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui); if (!mEnabled) return; - final ZenModeController zenController = new ZenModeControllerImpl(mContext, mHandler); - mVolumeComponent = new VolumeDialogComponent(this, mContext, null, zenController); + mVolumeComponent = new VolumeDialogComponent(this, mContext, null); putComponent(VolumeComponent.class, getVolumeComponent()); setDefaultVolumeController(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java new file mode 100644 index 000000000000..973f1f26033c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.os.Looper; + +import com.android.systemui.statusbar.policy.FlashlightController; + +import org.junit.Test; + +public class DependencyTest extends SysuiTestCase { + + @Test + public void testClassDependency() { + FlashlightController f = mock(FlashlightController.class); + injectTestDependency(FlashlightController.class, f); + assertEquals(f, Dependency.get(FlashlightController.class)); + } + + @Test + public void testStringDependency() { + Looper l = Looper.getMainLooper(); + injectTestDependency(Dependency.BG_LOOPER, l); + assertEquals(l, Dependency.get(Dependency.BG_LOOPER)); + } + + @Test + public void testDump() { + Dumpable d = mock(Dumpable.class); + injectTestDependency("test", d); + Dependency.get("test"); + mDependency.dump(null, null, null); + verify(d).dump(eq(null), eq(null), eq(null)); + } + + @Test + public void testConfigurationChanged() { + ConfigurationChangedReceiver d = mock(ConfigurationChangedReceiver.class); + injectTestDependency("test", d); + Dependency.get("test"); + mDependency.onConfigurationChanged(null); + verify(d).onConfigurationChanged(eq(null)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java index d1d752099d19..5fe5174c3d27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -15,11 +15,14 @@ */ package com.android.systemui; +import static org.mockito.Mockito.mock; + import android.content.Context; import android.support.test.InstrumentationRegistry; import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; +import android.util.ArrayMap; import com.android.systemui.utils.TestableContext; import com.android.systemui.utils.leaks.Tracker; @@ -34,11 +37,15 @@ public abstract class SysuiTestCase { private Handler mHandler; protected TestableContext mContext; + protected TestDependency mDependency; @Before public void SysuiSetup() throws Exception { System.setProperty("dexmaker.share_classloader", "true"); mContext = new TestableContext(InstrumentationRegistry.getTargetContext(), this); + mDependency = new TestDependency(); + mDependency.mContext = mContext; + mDependency.start(); } @After @@ -78,11 +85,37 @@ public abstract class SysuiTestCase { return null; } + public void injectMockDependency(Class<?> cls) { + mDependency.injectTestDependency(cls.getName(), mock(cls)); + } + + public void injectTestDependency(Class<?> cls, Object obj) { + mDependency.injectTestDependency(cls.getName(), obj); + } + + public void injectTestDependency(String key, Object obj) { + mDependency.injectTestDependency(key, obj); + } + public static final class EmptyRunnable implements Runnable { public void run() { } } + public static class TestDependency extends Dependency { + private final ArrayMap<String, Object> mObjs = new ArrayMap<>(); + + private void injectTestDependency(String key, Object obj) { + mObjs.put(key, obj); + } + + @Override + protected <T> T createDependency(String cls) { + if (mObjs.containsKey(cls)) return (T) mObjs.get(cls); + return super.createDependency(cls); + } + } + public static final class Idler implements MessageQueue.IdleHandler { private final Runnable mCallback; private boolean mIdle; diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java index d529ee105dd9..3715df2a42c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java @@ -113,8 +113,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { waitForIdleSync(mPluginInstanceManager.mPluginHandler); waitForIdleSync(mPluginInstanceManager.mMainHandler); - verify(mMockListener, Mockito.never()).onPluginConnected( - ArgumentCaptor.forClass(Plugin.class).capture()); + verify(mMockListener, Mockito.never()).onPluginConnected(any(), any()); } @Test @@ -124,7 +123,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { // Verify startup lifecycle verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(), ArgumentCaptor.forClass(Context.class).capture()); - verify(mMockListener).onPluginConnected(ArgumentCaptor.forClass(Plugin.class).capture()); + verify(mMockListener).onPluginConnected(any(), any()); } @Test @@ -154,8 +153,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { waitForIdleSync(mPluginInstanceManager.mMainHandler); // Plugin shouldn't be connected because it is the wrong version. - verify(mMockListener, Mockito.never()).onPluginConnected( - ArgumentCaptor.forClass(Plugin.class).capture()); + verify(mMockListener, Mockito.never()).onPluginConnected(any(), any()); verify(nm).notifyAsUser(eq(TestPlugin.class.getName()), eq(SystemMessage.NOTE_PLUGIN), any(), eq(UserHandle.ALL)); } @@ -176,8 +174,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { verify(sMockPlugin, Mockito.times(2)).onCreate( ArgumentCaptor.forClass(Context.class).capture(), ArgumentCaptor.forClass(Context.class).capture()); - verify(mMockListener, Mockito.times(2)).onPluginConnected( - ArgumentCaptor.forClass(Plugin.class).capture()); + verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any()); } @Test @@ -193,8 +190,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { waitForIdleSync(mPluginInstanceManager.mMainHandler);; // Non-debuggable build should receive no plugins. - verify(mMockListener, Mockito.never()).onPluginConnected( - ArgumentCaptor.forClass(Plugin.class).capture()); + verify(mMockListener, Mockito.never()).onPluginConnected(any(), any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java index 8acd6ba5e1d7..2f6487b27c7e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java @@ -29,6 +29,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.SecurityController; @@ -56,6 +57,8 @@ public class QSFooterTest extends SysuiTestCase { @Before public void setUp() { + injectTestDependency(SecurityController.class, mSecurityController); + injectTestDependency(Dependency.BG_LOOPER, Looper.getMainLooper()); mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) @@ -67,7 +70,7 @@ public class QSFooterTest extends SysuiTestCase { mFooterText = (TextView) mRootView.findViewById(R.id.footer_text); mFooterIcon = (TestableImageView) mRootView.findViewById(R.id.footer_icon); mFooterIcon2 = (TestableImageView) mRootView.findViewById(R.id.footer_icon2); - mFooter.setHostEnvironment(null, mSecurityController, Looper.getMainLooper()); + mFooter.setHostEnvironment(null); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index c0d5bbd35690..e3ee8514fe3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -18,11 +18,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.os.Handler; +import android.os.Looper; import android.support.test.runner.AndroidJUnit4; +import com.android.systemui.Dependency; import com.android.systemui.FragmentTestCase; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.phone.QuickStatusBarHeader; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -42,6 +43,7 @@ import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,33 +56,24 @@ public class QSFragmentTest extends FragmentTestCase { super(QSFragment.class); } + @Before + public void addLeakCheckDependencies() { + injectMockDependency(UserSwitcherController.class); + injectLeakCheckedDependencies(BluetoothController.class, LocationController.class, + RotationLockController.class, NetworkController.class, ZenModeController.class, + HotspotController.class, CastController.class, FlashlightController.class, + UserInfoController.class, KeyguardMonitor.class, SecurityController.class, + BatteryController.class, NextAlarmController.class); + } + @Test public void testListening() { QSFragment qs = (QSFragment) mFragment; postAndWait(() -> mFragments.dispatchResume()); - UserSwitcherController userSwitcher = mock(UserSwitcherController.class); - KeyguardMonitor keyguardMonitor = getLeakChecker(KeyguardMonitor.class); - when(userSwitcher.getKeyguardMonitor()).thenReturn(keyguardMonitor); - when(userSwitcher.getUsers()).thenReturn(new ArrayList<>()); - QSTileHost host = new QSTileHost(mContext, - null, - getLeakChecker(BluetoothController.class), - getLeakChecker(LocationController.class), - getLeakChecker(RotationLockController.class), - getLeakChecker(NetworkController.class), - getLeakChecker(ZenModeController.class), - getLeakChecker(HotspotController.class), - getLeakChecker(CastController.class), - getLeakChecker(FlashlightController.class), - userSwitcher, - getLeakChecker(UserInfoController.class), - keyguardMonitor, - getLeakChecker(SecurityController.class), - getLeakChecker(BatteryController.class), - mock(StatusBarIconController.class), - getLeakChecker(NextAlarmController.class)); + QSTileHost host = new QSTileHost(mContext, null, + mock(StatusBarIconController.class)); qs.setHost(host); - Handler h = new Handler(host.getLooper()); + Handler h = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER)); qs.setListening(true); waitForIdleSync(h); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java index 3ee13727f037..4146cb81873d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java @@ -47,13 +47,7 @@ public class TileServicesTest extends SysuiTestCase { @Before public void setUp() throws Exception { mManagers = new ArrayList<>(); - final NetworkController networkController = Mockito.mock(NetworkController.class); - Mockito.when(networkController.getDataSaverController()).thenReturn( - Mockito.mock(DataSaverController.class)); - QSTileHost host = new QSTileHost(mContext, null, null, null, null, - networkController, null, - Mockito.mock(HotspotController.class), null, - null, null, null, null, null, null, null, null); + QSTileHost host = new QSTileHost(mContext, null, null); mTileService = new TestTileServices(host, Looper.getMainLooper()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java index e140e98aee29..9fcb5f75ff22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java @@ -47,10 +47,6 @@ public class NavigationBarFragmentTest extends FragmentTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mock(WindowManager.class)); NavigationBarFragment navigationBarFragment = (NavigationBarFragment) mFragment; - AssistManager assistManager = new AssistManager(mContext.getComponent(PhoneStatusBar.class), - mContext); - navigationBarFragment.setAssistManager(assistManager); - postAndWait(() -> mFragments.dispatchResume()); navigationBarFragment.onHomeLongClick(navigationBarFragment.getView()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTest.java new file mode 100644 index 000000000000..d82566f0bb46 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.keyguard.KeyguardHostView.OnDismissAction; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.BaseStatusBar; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PhoneStatusBarTest extends SysuiTestCase { + + StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + PhoneStatusBar mPhoneStatusBar; + + @Before + public void setup() { + mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager.class); + mPhoneStatusBar = new TestablePhoneStatusBar(mStatusBarKeyguardViewManager); + + doAnswer(invocation -> { + OnDismissAction onDismissAction = (OnDismissAction) invocation.getArguments()[0]; + onDismissAction.onDismiss(); + return null; + }).when(mStatusBarKeyguardViewManager).dismissWithAction(any(), any(), anyBoolean()); + + doAnswer(invocation -> { + Runnable runnable = (Runnable) invocation.getArguments()[0]; + runnable.run(); + return null; + }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); + } + + @Test + public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() { + when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true); + + mPhoneStatusBar.executeRunnableDismissingKeyguard(null, null, false, false, false); + } + + @Test + public void executeRunnableDismissingKeyguard_nullRunnable_showing() { + when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + + mPhoneStatusBar.executeRunnableDismissingKeyguard(null, null, false, false, false); + } + + @Test + public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() { + when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + + mPhoneStatusBar.executeRunnableDismissingKeyguard(null, null, false, false, false); + } + + static class TestablePhoneStatusBar extends PhoneStatusBar { + public TestablePhoneStatusBar(StatusBarKeyguardViewManager man) { + mStatusBarKeyguardViewManager = man; + } + + @Override + protected BaseStatusBar.H createHandler() { + return null; + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java index 6fe776842eaf..23c635c3e93f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java @@ -118,7 +118,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mNetworkController = new NetworkControllerImpl(mContext, mMockCm, mMockTm, mMockWm, mMockSm, mConfig, Looper.getMainLooper(), mCallbackHandler, mock(AccessPointControllerImpl.class), mock(DataUsageController.class), - mMockSubDefaults); + mMockSubDefaults, mock(DeviceProvisionedController.class)); setupNetworkController(); // Trigger blank callbacks to always get the current state (some tests don't trigger @@ -160,7 +160,8 @@ public class NetworkControllerBaseTest extends SysuiTestCase { = new NetworkControllerImpl(mContext, mMockCm, mMockTm, mMockWm, mMockSm, mConfig, mContext.getMainLooper(), mCallbackHandler, mock(AccessPointControllerImpl.class), - mock(DataUsageController.class), mMockSubDefaults); + mock(DataUsageController.class), mMockSubDefaults, + mock(DeviceProvisionedController.class)); setupNetworkController(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java index 4f961abffe6a..1f7ec1aecd16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java @@ -1,5 +1,7 @@ package com.android.systemui.statusbar.policy; +import static org.mockito.Mockito.mock; + import android.net.NetworkCapabilities; import android.os.Looper; import android.support.test.runner.AndroidJUnit4; @@ -100,8 +102,9 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { mConfig.show4gForLte = true; mNetworkController = new NetworkControllerImpl(mContext, mMockCm, mMockTm, mMockWm, mMockSm, mConfig, Looper.getMainLooper(), mCallbackHandler, - Mockito.mock(AccessPointControllerImpl.class), - Mockito.mock(DataUsageController.class), mMockSubDefaults); + mock(AccessPointControllerImpl.class), + mock(DataUsageController.class), mMockSubDefaults, + mock(DeviceProvisionedController.class)); setupNetworkController(); setupDefaultSignal(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java index 4560aa73b7c4..1a61d80e24f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java @@ -56,7 +56,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mNetworkController = new NetworkControllerImpl(mContext, mMockCm, mMockTm, mMockWm, mMockSm, mConfig, Looper.getMainLooper(), mCallbackHandler, mock(AccessPointControllerImpl.class), mock(DataUsageController.class), - mMockSubDefaults); + mMockSubDefaults, mock(DeviceProvisionedController.class)); setupNetworkController(); verifyLastMobileDataIndicators(false, 0, 0); @@ -110,7 +110,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mNetworkController = new NetworkControllerImpl(mContext, mMockCm, mMockTm, mMockWm, mMockSm, mConfig, Looper.getMainLooper(), mCallbackHandler, mock(AccessPointControllerImpl.class), mock(DataUsageController.class), - mMockSubDefaults); + mMockSubDefaults, mock(DeviceProvisionedController.class)); setupNetworkController(); // No Subscriptions. diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java index 0238bf7cb004..b118fdcb64ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java @@ -14,9 +14,13 @@ package com.android.systemui.utils.leaks; +import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.CallbackController; -public class BaseLeakChecker<T> implements CallbackController<T> { +import java.io.FileDescriptor; +import java.io.PrintWriter; + +public class BaseLeakChecker<T> implements CallbackController<T>, Dumpable { private final Tracker mTracker; @@ -37,4 +41,9 @@ public class BaseLeakChecker<T> implements CallbackController<T> { public void removeCallback(T listener) { mTracker.getLeakInfo(listener).clearAllocations(); } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java index fcfe9aa8f5d2..5497686da865 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java @@ -14,11 +14,16 @@ package com.android.systemui.utils.leaks; +import android.os.Bundle; + import com.android.settingslib.net.DataUsageController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import java.io.FileDescriptor; +import java.io.PrintWriter; + public class FakeNetworkController extends BaseLeakChecker<SignalCallback> implements NetworkController { @@ -42,22 +47,32 @@ public class FakeNetworkController extends BaseLeakChecker<SignalCallback> } @Override - public DataSaverController getDataSaverController() { - return mDataSaverController; + public void setUserSetupComplete(boolean userSetup) { + } @Override - public boolean hasMobileDataFeature() { + public boolean hasEmergencyCryptKeeperText() { return false; } @Override - public void setWifiEnabled(boolean enabled) { + public boolean isRadioOn() { + return false; + } + + @Override + public DataSaverController getDataSaverController() { + return mDataSaverController; + } + @Override + public boolean hasMobileDataFeature() { + return false; } @Override - public void onUserSwitched(int newUserId) { + public void setWifiEnabled(boolean enabled) { } @@ -75,4 +90,9 @@ public class FakeNetworkController extends BaseLeakChecker<SignalCallback> public boolean hasVoiceCallingFeature() { return false; } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java index 13ea385d6c6e..7581363c78f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java @@ -53,11 +53,6 @@ public class FakeZenModeController extends BaseLeakChecker<Callback> implements } @Override - public void setUserId(int userId) { - - } - - @Override public boolean isZenAvailable() { return false; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java index 728ed606cffb..c182493cf570 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java @@ -118,6 +118,12 @@ public abstract class LeakCheckedTest extends SysuiTestCase { mTrackers.values().forEach(Tracker::verify); } + public void injectLeakCheckedDependencies(Class<?>... cls) { + for (Class<?> c : cls) { + injectTestDependency(c, getLeakChecker(c)); + } + } + public <T extends CallbackController> T addListening(T mock, Class<T> cls, String tag) { doAnswer(new Answer<Void>() { @Override diff --git a/preloaded-classes b/preloaded-classes index 86cbb69bdd9e..2fad5dd04a82 100644 --- a/preloaded-classes +++ b/preloaded-classes @@ -823,11 +823,6 @@ android.graphics.DrawFilter android.graphics.EmbossMaskFilter android.graphics.FontFamily android.graphics.FontListParser -android.graphics.FontListParser$Alias -android.graphics.FontListParser$Axis -android.graphics.FontListParser$Config -android.graphics.FontListParser$Family -android.graphics.FontListParser$Font android.graphics.Insets android.graphics.Interpolator android.graphics.Interpolator$Result @@ -1001,10 +996,6 @@ android.hardware.input.InputDeviceIdentifier android.hardware.input.InputDeviceIdentifier$1 android.hardware.input.InputManager android.hardware.input.InputManager$InputDevicesChangedListener -# These cannot be preloaded and need to be refactored into system server. b/17791590, b/21935130 -# android.hardware.location.ActivityRecognitionHardware -# android.hardware.location.IActivityRecognitionHardware -# android.hardware.location.IActivityRecognitionHardware$Stub android.hardware.location.ContextHubManager android.hardware.location.IContextHubService android.hardware.location.IContextHubService$Stub @@ -1847,6 +1838,11 @@ android.text.DynamicLayout android.text.DynamicLayout$ChangeWatcher android.text.Editable android.text.Editable$Factory +android.text.FontConfig +android.text.FontConfig$Alias +android.text.FontConfig$Axis +android.text.FontConfig$Family +android.text.FontConfig$Font android.text.GetChars android.text.GraphicsOperations android.text.Html diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java index 581aa05d7d32..0e07ec0e27f4 100644 --- a/services/core/java/com/android/server/AlarmManagerService.java +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -504,8 +504,8 @@ class AlarmManagerService extends SystemService { for (int i = alarms.size()-1; i >= 0; i--) { Alarm alarm = alarms.get(i); try { - if (alarm.uid == uid && ActivityManager.getService().getAppStartMode( - uid, alarm.packageName) == ActivityManager.APP_START_MODE_DISABLED) { + if (alarm.uid == uid && ActivityManager.getService().isAppStartModeDisabled( + uid, alarm.packageName)) { alarms.remove(i); didRemove = true; if (alarm.alarmClock != null) { @@ -1089,8 +1089,7 @@ class AlarmManagerService extends SystemService { operation, directReceiver, listenerTag, workSource, flags, alarmClock, callingUid, callingPackage); try { - if (ActivityManager.getService().getAppStartMode(callingUid, callingPackage) - == ActivityManager.APP_START_MODE_DISABLED) { + if (ActivityManager.getService().isAppStartModeDisabled(callingUid, callingPackage)) { Slog.w(TAG, "Not setting alarm from " + callingUid + ":" + a + " -- package not allowed to start"); return; diff --git a/services/core/java/com/android/server/FontManagerService.java b/services/core/java/com/android/server/FontManagerService.java new file mode 100644 index 000000000000..593c3220fdb5 --- /dev/null +++ b/services/core/java/com/android/server/FontManagerService.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.graphics.FontListParser; +import android.os.ParcelFileDescriptor; +import android.text.FontConfig; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.font.IFontManager; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class FontManagerService extends IFontManager.Stub { + private static final String TAG = "FontManagerService"; + private static final String FONTS_CONFIG = "/system/etc/fonts.xml"; + + @GuardedBy("mLock") + private FontConfig mConfig; + private final Object mLock = new Object(); + + public static final class Lifecycle extends SystemService { + private final FontManagerService mService; + + public Lifecycle(Context context) { + super(context); + mService = new FontManagerService(); + } + + @Override + public void onStart() { + try { + publishBinderService(Context.FONT_SERVICE, mService); + } catch (Throwable t) { + // Starting this service is not critical to the running of this device and should + // therefore not crash the device. If it fails, log the error and continue. + Slog.e(TAG, "Could not start the FontManagerService.", t); + } + } + } + + @Override + public FontConfig getSystemFonts() { + synchronized (mLock) { + if (mConfig != null) { + return new FontConfig(mConfig); + } + + FontConfig config = loadFromSystem(); + if (config == null) { + return null; + } + + final int size = config.getFamilies().size(); + for (int i = 0; i < size; ++i) { + FontConfig.Family family = config.getFamilies().get(i); + for (int j = 0; j < family.getFonts().size(); ++j) { + FontConfig.Font font = family.getFonts().get(j); + File fontFile = new File(font.getFontName()); + try { + font.setFd(ParcelFileDescriptor.open( + fontFile, ParcelFileDescriptor.MODE_READ_ONLY)); + } catch (IOException e) { + Slog.e(TAG, "Error opening font file " + font.getFontName(), e); + } + } + } + + mConfig = config; + return new FontConfig(mConfig); + } + } + + private FontConfig loadFromSystem() { + File configFilename = new File(FONTS_CONFIG); + try { + FileInputStream fontsIn = new FileInputStream(configFilename); + return FontListParser.parse(fontsIn); + } catch (IOException | XmlPullParserException e) { + Slog.e(TAG, "Error opening " + configFilename, e); + } + return null; + } + + public FontManagerService() { + } +} diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index f718fa11598e..bee1f9729eea 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -243,7 +243,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub private PendingIntent mImeSwitchPendingIntent; private boolean mShowOngoingImeSwitcherForPhones; private boolean mNotificationShown; - private final boolean mImeSelectedOnBoot; static class SessionState { final ClientState client; @@ -566,7 +565,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } } - class ImmsBroadcastReceiver extends android.content.BroadcastReceiver { + class ImmsBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -587,6 +586,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub Intent.EXTRA_SETTING_NEW_VALUE); restoreEnabledInputMethods(mContext, prevValue, newValue); } + } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { + synchronized (mMethodMap) { + resetStateIfCurrentLocaleChangedLocked(); + } } else { Slog.w(TAG, "Unexpected intent " + intent); } @@ -845,9 +848,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub return; } mSettings.switchCurrentUser(currentUserId, !mSystemReady); - // We need to rebuild IMEs. - buildInputMethodListLocked(false /* resetDefaultEnabledIme */); - updateInputMethodsFromSettingsLocked(true /* enabledChanged */); + if (mSystemReady) { + // We need to rebuild IMEs. + buildInputMethodListLocked(false /* resetDefaultEnabledIme */); + updateInputMethodsFromSettingsLocked(true /* enabledChanged */); + } } } @@ -897,13 +902,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mShowOngoingImeSwitcherForPhones = false; - final IntentFilter broadcastFilter = new IntentFilter(); - broadcastFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - broadcastFilter.addAction(Intent.ACTION_USER_ADDED); - broadcastFilter.addAction(Intent.ACTION_USER_REMOVED); - broadcastFilter.addAction(Intent.ACTION_SETTING_RESTORED); - mContext.registerReceiver(new ImmsBroadcastReceiver(), broadcastFilter); - mNotificationShown = false; int userId = 0; try { @@ -911,7 +909,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } catch (RemoteException e) { Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e); } - mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true); // mSettings should be created before buildInputMethodListLocked mSettings = new InputMethodSettings( @@ -919,48 +916,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub updateCurrentProfileIds(); mFileManager = new InputMethodFileManager(mMethodMap, userId); - synchronized (mMethodMap) { - mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mSettings, context); - } - - // Just checking if defaultImiId is empty or not - final String defaultImiId = mSettings.getSelectedInputMethod(); - if (DEBUG) { - Slog.d(TAG, "Initial default ime = " + defaultImiId); - } - mImeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - - synchronized (mMethodMap) { - buildInputMethodListLocked(!mImeSelectedOnBoot /* resetDefaultEnabledIme */); - } - mSettings.enableAllIMEsIfThereIsNoEnabledIME(); - - if (!mImeSelectedOnBoot) { - Slog.w(TAG, "No IME selected. Choose the most applicable IME."); - synchronized (mMethodMap) { - resetDefaultImeLocked(context); - } - } - - synchronized (mMethodMap) { - mSettingsObserver.registerContentObserverLocked(userId); - updateFromSettingsLocked(true); - } - - // IMMS wants to receive Intent.ACTION_LOCALE_CHANGED in order to update the current IME - // according to the new system locale. - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_LOCALE_CHANGED); - mContext.registerReceiver( - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - synchronized(mMethodMap) { - resetStateIfCurrentLocaleChangedLocked(); - } - } - }, filter); + mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( + mSettings, context); } private void resetDefaultImeLocked(Context context) { @@ -1089,6 +1046,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } if (!mSystemReady) { mSystemReady = true; + mLastSystemLocales = mRes.getConfiguration().getLocales(); final int currentUserId = mSettings.getCurrentUserId(); mSettings.switchCurrentUser(currentUserId, !mUserManager.isUserUnlockingOrUnlocked(currentUserId)); @@ -1105,14 +1063,25 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mWindowManagerInternal.setOnHardKeyboardStatusChangeListener( mHardKeyboardListener); } - if (!mImeSelectedOnBoot) { - Slog.w(TAG, "Reset the default IME as \"Resource\" is ready here."); - resetStateIfCurrentLocaleChangedLocked(); - InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mIPackageManager, - mSettings.getEnabledInputMethodListLocked(), - mSettings.getCurrentUserId(), mContext.getBasePackageName()); - } - mLastSystemLocales = mRes.getConfiguration().getLocales(); + + mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true); + mSettingsObserver.registerContentObserverLocked(currentUserId); + + final IntentFilter broadcastFilter = new IntentFilter(); + broadcastFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + broadcastFilter.addAction(Intent.ACTION_USER_ADDED); + broadcastFilter.addAction(Intent.ACTION_USER_REMOVED); + broadcastFilter.addAction(Intent.ACTION_SETTING_RESTORED); + broadcastFilter.addAction(Intent.ACTION_LOCALE_CHANGED); + mContext.registerReceiver(new ImmsBroadcastReceiver(), broadcastFilter); + + buildInputMethodListLocked(true /* resetDefaultEnabledIme */); + resetDefaultImeLocked(mContext); + updateFromSettingsLocked(true); + InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mIPackageManager, + mSettings.getEnabledInputMethodListLocked(), currentUserId, + mContext.getBasePackageName()); + try { startInputInnerLocked(); } catch (RuntimeException e) { @@ -2624,6 +2593,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // additional input method subtypes to the IME. if (TextUtils.isEmpty(imiId) || subtypes == null) return; synchronized (mMethodMap) { + if (!mSystemReady) { + return; + } final InputMethodInfo imi = mMethodMap.get(imiId); if (imi == null) return; final String[] packageInfos; @@ -3048,6 +3020,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme + " \n ------ caller=" + Debug.getCallers(10)); } + if (!mSystemReady) { + Slog.e(TAG, "buildInputMethodListLocked is not allowed until system is ready"); + return; + } mMethodList.clear(); mMethodMap.clear(); diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index c9b59adeb2da..6cc72deb7faa 100644 --- a/services/core/java/com/android/server/LocationManagerService.java +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -16,6 +16,7 @@ package com.android.server; +import android.app.ActivityManager; import android.content.pm.PackageManagerInternal; import com.android.internal.content.PackageMonitor; import com.android.internal.location.ProviderProperties; @@ -138,6 +139,9 @@ public class LocationManagerService extends ILocationManager.Stub { // The maximum interval a location request can have and still be considered "high power". private static final long HIGH_POWER_INTERVAL_MS = 5 * 60 * 1000; + // default background throttling interval if not overriden in settings + private static final long DEFAULT_BACKGROUND_THROTTLE_INTERVAL_MS = 30 * 1000; + // Location Providers may sometimes deliver location updates // slightly faster that requested - provide grace period so // we don't unnecessarily filter events that are otherwise on @@ -157,6 +161,7 @@ public class LocationManagerService extends ILocationManager.Stub { private GeofenceManager mGeofenceManager; private PackageManager mPackageManager; private PowerManager mPowerManager; + private ActivityManager mActivityManager; private UserManager mUserManager; private GeocoderProxy mGeocodeProvider; private IGnssStatusProvider mGnssStatusProvider; @@ -171,47 +176,47 @@ public class LocationManagerService extends ILocationManager.Stub { // --- fields below are protected by mLock --- // Set of providers that are explicitly enabled // Only used by passive, fused & test. Network & GPS are controlled separately, and not listed. - private final Set<String> mEnabledProviders = new HashSet<String>(); + private final Set<String> mEnabledProviders = new HashSet<>(); // Set of providers that are explicitly disabled - private final Set<String> mDisabledProviders = new HashSet<String>(); + private final Set<String> mDisabledProviders = new HashSet<>(); // Mock (test) providers private final HashMap<String, MockProvider> mMockProviders = - new HashMap<String, MockProvider>(); + new HashMap<>(); // all receivers - private final HashMap<Object, Receiver> mReceivers = new HashMap<Object, Receiver>(); + private final HashMap<Object, Receiver> mReceivers = new HashMap<>(); // currently installed providers (with mocks replacing real providers) private final ArrayList<LocationProviderInterface> mProviders = - new ArrayList<LocationProviderInterface>(); + new ArrayList<>(); // real providers, saved here when mocked out private final HashMap<String, LocationProviderInterface> mRealProviders = - new HashMap<String, LocationProviderInterface>(); + new HashMap<>(); // mapping from provider name to provider private final HashMap<String, LocationProviderInterface> mProvidersByName = - new HashMap<String, LocationProviderInterface>(); + new HashMap<>(); // mapping from provider name to all its UpdateRecords private final HashMap<String, ArrayList<UpdateRecord>> mRecordsByProvider = - new HashMap<String, ArrayList<UpdateRecord>>(); + new HashMap<>(); private final LocationRequestStatistics mRequestStatistics = new LocationRequestStatistics(); // mapping from provider name to last known location - private final HashMap<String, Location> mLastLocation = new HashMap<String, Location>(); + private final HashMap<String, Location> mLastLocation = new HashMap<>(); // same as mLastLocation, but is not updated faster than LocationFudger.FASTEST_INTERVAL_MS. // locations stored here are not fudged for coarse permissions. private final HashMap<String, Location> mLastLocationCoarseInterval = - new HashMap<String, Location>(); + new HashMap<>(); // all providers that operate over proxy, for authorizing incoming location private final ArrayList<LocationProviderProxy> mProxyProviders = - new ArrayList<LocationProviderProxy>(); + new ArrayList<>(); // current active user on the device - other users are denied location data private int mCurrentUserId = UserHandle.USER_SYSTEM; @@ -252,6 +257,10 @@ public class LocationManagerService extends ILocationManager.Stub { // fetch power manager mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + // fetch activity manager + mActivityManager + = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + // prepare worker thread mLocationHandler = new LocationWorkerHandler(BackgroundThread.get().getLooper()); @@ -286,6 +295,40 @@ public class LocationManagerService extends ILocationManager.Stub { }; mPackageManager.addOnPermissionsChangeListener(permissionListener); + // listen for background/foreground changes + ActivityManager.OnUidImportanceListener uidImportanceListener + = new ActivityManager.OnUidImportanceListener() { + @Override + public void onUidImportance(int uid, int importance) { + boolean foreground = isImportanceForeground(importance); + HashSet<String> affectedProviders = new HashSet<>(mRecordsByProvider.size()); + synchronized (mLock) { + for (Map.Entry<String, ArrayList<UpdateRecord>> entry + : mRecordsByProvider.entrySet()) { + String provider = entry.getKey(); + for (UpdateRecord record : entry.getValue()) { + if (record.mReceiver.mUid == uid + && record.mIsForegroundUid != foreground) { + if (D) Log.d(TAG, "request from uid " + uid + " is now " + + (foreground ? "foreground" : "background)")); + record.mIsForegroundUid = foreground; + + if (!isThrottlingExemptLocked(record.mReceiver)) { + affectedProviders.add(provider); + } + } + } + } + for (String provider : affectedProviders) { + applyRequirementsLocked(provider); + } + } + + } + }; + mActivityManager.addOnUidImportanceListener(uidImportanceListener, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); updateUserProfiles(mCurrentUserId); @@ -305,6 +348,17 @@ public class LocationManagerService extends ILocationManager.Stub { } } }, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS), + true, + new ContentObserver(mLocationHandler) { + @Override + public void onChange(boolean selfChange) { + synchronized (mLock) { + updateProvidersLocked(); + } + } + }, UserHandle.USER_ALL); mPackageMonitor.register(mContext, mLocationHandler.getLooper(), true); // listen for user change @@ -334,6 +388,10 @@ public class LocationManagerService extends ILocationManager.Stub { }, UserHandle.ALL, intentFilter, null, mLocationHandler); } + private static boolean isImportanceForeground(int importance) { + return importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; + } + /** * Provides a way for components held by the {@link LocationManagerService} to clean-up * gracefully on system's shutdown. @@ -483,7 +541,7 @@ public class LocationManagerService extends ILocationManager.Stub { that matches the signature of at least one package on this list. */ Resources resources = mContext.getResources(); - ArrayList<String> providerPackageNames = new ArrayList<String>(); + ArrayList<String> providerPackageNames = new ArrayList<>(); String[] pkgs = resources.getStringArray( com.android.internal.R.array.config_locationProviderPackageNames); if (D) Log.d(TAG, "certificates for location providers pulled from: " + @@ -651,7 +709,7 @@ public class LocationManagerService extends ILocationManager.Stub { final boolean mHideFromAppOps; // True if AppOps should not monitor this receiver. final Object mKey; - final HashMap<String,UpdateRecord> mUpdateRecords = new HashMap<String,UpdateRecord>(); + final HashMap<String,UpdateRecord> mUpdateRecords = new HashMap<>(); // True if app ops has started monitoring this receiver for locations. boolean mOpMonitoring; @@ -691,10 +749,7 @@ public class LocationManagerService extends ILocationManager.Stub { @Override public boolean equals(Object otherObj) { - if (otherObj instanceof Receiver) { - return mKey.equals(((Receiver)otherObj).mKey); - } - return false; + return (otherObj instanceof Receiver) && mKey.equals(((Receiver) otherObj).mKey); } @Override @@ -1011,13 +1066,25 @@ public class LocationManagerService extends ILocationManager.Stub { mProvidersByName.remove(provider.getName()); } + private boolean isOverlayProviderPackageLocked(String packageName) { + for (LocationProviderInterface provider : mProviders) { + if (provider instanceof LocationProviderProxy) { + if (packageName.equals( + ((LocationProviderProxy) provider).getConnectedPackageName())) { + return true; + } + } + } + + return false; + } + /** * Returns "true" if access to the specified location provider is allowed by the current * user's settings. Access to all location providers is forbidden to non-location-provider * processes belonging to background users. * * @param provider the name of the location provider - * @return */ private boolean isAllowedByCurrentUserSettingsLocked(String provider) { if (mEnabledProviders.contains(provider)) { @@ -1039,7 +1106,6 @@ public class LocationManagerService extends ILocationManager.Stub { * * @param provider the name of the location provider * @param uid the requestor's UID - * @return */ private boolean isAllowedByUserSettingsLocked(String provider, int uid) { if (!isCurrentProfile(UserHandle.getUserId(uid)) && !isUidALocationProvider(uid)) { @@ -1197,11 +1263,7 @@ public class LocationManagerService extends ILocationManager.Stub { } } - if (getAllowedResolutionLevel(pid, uid) < allowedResolutionLevel) { - return false; - } - - return true; + return getAllowedResolutionLevel(pid, uid) >= allowedResolutionLevel; } boolean checkLocationAccess(int pid, int uid, String packageName, int allowedResolutionLevel) { @@ -1212,11 +1274,7 @@ public class LocationManagerService extends ILocationManager.Stub { } } - if (getAllowedResolutionLevel(pid, uid) < allowedResolutionLevel) { - return false; - } - - return true; + return getAllowedResolutionLevel(pid, uid) >= allowedResolutionLevel; } /** @@ -1228,7 +1286,7 @@ public class LocationManagerService extends ILocationManager.Stub { public List<String> getAllProviders() { ArrayList<String> out; synchronized (mLock) { - out = new ArrayList<String>(mProviders.size()); + out = new ArrayList<>(mProviders.size()); for (LocationProviderInterface provider : mProviders) { String name = provider.getName(); if (LocationManager.FUSED_PROVIDER.equals(name)) { @@ -1251,11 +1309,11 @@ public class LocationManagerService extends ILocationManager.Stub { public List<String> getProviders(Criteria criteria, boolean enabledOnly) { int allowedResolutionLevel = getCallerAllowedResolutionLevel(); ArrayList<String> out; - int uid = Binder.getCallingUid();; + int uid = Binder.getCallingUid(); long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - out = new ArrayList<String>(mProviders.size()); + out = new ArrayList<>(mProviders.size()); for (LocationProviderInterface provider : mProviders) { String name = provider.getName(); if (LocationManager.FUSED_PROVIDER.equals(name)) { @@ -1370,14 +1428,12 @@ public class LocationManagerService extends ILocationManager.Stub { ArrayList<UpdateRecord> records = mRecordsByProvider.get(provider); if (records != null) { - final int N = records.size(); - for (int i = 0; i < N; i++) { - UpdateRecord record = records.get(i); + for (UpdateRecord record : records) { if (isCurrentProfile(UserHandle.getUserId(record.mReceiver.mUid))) { // Sends a notification message to the receiver if (!record.mReceiver.callProviderEnabledLocked(provider, enabled)) { if (deadReceivers == null) { - deadReceivers = new ArrayList<Receiver>(); + deadReceivers = new ArrayList<>(); } deadReceivers.add(record.mReceiver); } @@ -1410,6 +1466,12 @@ public class LocationManagerService extends ILocationManager.Stub { WorkSource worksource = new WorkSource(); ProviderRequest providerRequest = new ProviderRequest(); + ContentResolver resolver = mContext.getContentResolver(); + long backgroundThrottleInterval = Settings.Global.getLong( + resolver, + Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS, + DEFAULT_BACKGROUND_THROTTLE_INTERVAL_MS); + if (records != null) { for (UpdateRecord record : records) { if (isCurrentProfile(UserHandle.getUserId(record.mReceiver.mUid))) { @@ -1419,10 +1481,22 @@ public class LocationManagerService extends ILocationManager.Stub { record.mReceiver.mPackageName, record.mReceiver.mAllowedResolutionLevel)) { LocationRequest locationRequest = record.mRequest; + long interval = locationRequest.getInterval(); + + if (!isThrottlingExemptLocked(record.mReceiver)) { + if (!record.mIsForegroundUid) { + interval = Math.max(interval, backgroundThrottleInterval); + } + if (interval != locationRequest.getInterval()) { + locationRequest = new LocationRequest(locationRequest); + locationRequest.setInterval(interval); + } + } + providerRequest.locationRequests.add(locationRequest); - if (locationRequest.getInterval() < providerRequest.interval) { + if (interval < providerRequest.interval) { providerRequest.reportLocation = true; - providerRequest.interval = locationRequest.getInterval(); + providerRequest.interval = interval; } } } @@ -1468,10 +1542,15 @@ public class LocationManagerService extends ILocationManager.Stub { p.setRequest(providerRequest, worksource); } + private boolean isThrottlingExemptLocked(Receiver recevier) { + return isOverlayProviderPackageLocked(recevier.mPackageName); + } + private class UpdateRecord { final String mProvider; final LocationRequest mRequest; final Receiver mReceiver; + boolean mIsForegroundUid; Location mLastFixBroadcast; long mLastStatusBroadcast; @@ -1482,10 +1561,12 @@ public class LocationManagerService extends ILocationManager.Stub { mProvider = provider; mRequest = request; mReceiver = receiver; + mIsForegroundUid = isImportanceForeground( + mActivityManager.getPackageImportance(mReceiver.mPackageName)); ArrayList<UpdateRecord> records = mRecordsByProvider.get(provider); if (records == null) { - records = new ArrayList<UpdateRecord>(); + records = new ArrayList<>(); mRecordsByProvider.put(provider, records); } if (!records.contains(this)) { @@ -1517,7 +1598,7 @@ public class LocationManagerService extends ILocationManager.Stub { receiverRecords.remove(this.mProvider); // and also remove the Receiver if it has no more update records - if (removeReceiver && receiverRecords.size() == 0) { + if (receiverRecords.size() == 0) { removeUpdatesLocked(mReceiver); } } @@ -1525,14 +1606,9 @@ public class LocationManagerService extends ILocationManager.Stub { @Override public String toString() { - StringBuilder s = new StringBuilder(); - s.append("UpdateRecord["); - s.append(mProvider); - s.append(' ').append(mReceiver.mPackageName).append('('); - s.append(mReceiver.mUid).append(')'); - s.append(' ').append(mRequest); - s.append(']'); - return s.toString(); + return "UpdateRecord[" + mProvider + " " + mReceiver.mPackageName + + "(" + mReceiver.mUid + (mIsForegroundUid ? " foreground" : " background") + + ")" + " " + mRequest + "]"; } } @@ -1681,14 +1757,17 @@ public class LocationManagerService extends ILocationManager.Stub { throw new IllegalArgumentException("provider name must not be null"); } - if (D) Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver)) - + " " + name + " " + request + " from " + packageName + "(" + uid + ")"); LocationProviderInterface provider = mProvidersByName.get(name); if (provider == null) { throw new IllegalArgumentException("provider doesn't exist: " + name); } UpdateRecord record = new UpdateRecord(name, request, receiver); + if (D) Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver)) + + " " + name + " " + request + " from " + packageName + "(" + uid + " " + + (record.mIsForegroundUid ? "foreground" : "background") + + (isOverlayProviderPackageLocked(receiver.mPackageName) ? " [whitelisted]" : "") + ")"); + UpdateRecord oldRecord = receiver.mUpdateRecords.put(name, record); if (oldRecord != null) { oldRecord.disposeLocked(false); @@ -1743,7 +1822,7 @@ public class LocationManagerService extends ILocationManager.Stub { receiver.updateMonitoring(false); // Record which providers were associated with this listener - HashSet<String> providers = new HashSet<String>(); + HashSet<String> providers = new HashSet<>(); HashMap<String, UpdateRecord> oldRecords = receiver.mUpdateRecords; if (oldRecords != null) { // Call dispose() on the obsolete update records. @@ -2084,9 +2163,7 @@ public class LocationManagerService extends ILocationManager.Stub { try { synchronized (mLock) { LocationProviderInterface p = mProvidersByName.get(provider); - if (p == null) return false; - - return isAllowedByUserSettingsLocked(provider, uid); + return p != null && isAllowedByUserSettingsLocked(provider, uid); } } finally { Binder.restoreCallingIdentity(identity); @@ -2197,11 +2274,7 @@ public class LocationManagerService extends ILocationManager.Stub { } // Check whether the expiry date has passed - if (record.mRequest.getExpireAt() < now) { - return false; - } - - return true; + return record.mRequest.getExpireAt() >= now; } private void handleLocationChangedLocked(Location location, boolean passive) { @@ -2216,7 +2289,7 @@ public class LocationManagerService extends ILocationManager.Stub { // Update last known locations Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); - Location lastNoGPSLocation = null; + Location lastNoGPSLocation; Location lastLocation = mLastLocation.get(provider); if (lastLocation == null) { lastLocation = new Location(provider); @@ -2296,7 +2369,7 @@ public class LocationManagerService extends ILocationManager.Stub { continue; } - Location notifyLocation = null; + Location notifyLocation; if (receiver.mAllowedResolutionLevel < RESOLUTION_LEVEL_FINE) { notifyLocation = coarseLocation; // use coarse location } else { @@ -2333,14 +2406,14 @@ public class LocationManagerService extends ILocationManager.Stub { // track expired records if (r.mRequest.getNumUpdates() <= 0 || r.mRequest.getExpireAt() < now) { if (deadUpdateRecords == null) { - deadUpdateRecords = new ArrayList<UpdateRecord>(); + deadUpdateRecords = new ArrayList<>(); } deadUpdateRecords.add(r); } // track dead receivers if (receiverDead) { if (deadReceivers == null) { - deadReceivers = new ArrayList<Receiver>(); + deadReceivers = new ArrayList<>(); } if (!deadReceivers.contains(receiver)) { deadReceivers.add(receiver); @@ -2417,7 +2490,7 @@ public class LocationManagerService extends ILocationManager.Stub { for (Receiver receiver : mReceivers.values()) { if (receiver.mPackageName.equals(packageName)) { if (deadReceivers == null) { - deadReceivers = new ArrayList<Receiver>(); + deadReceivers = new ArrayList<>(); } deadReceivers.add(receiver); } @@ -2694,6 +2767,13 @@ public class LocationManagerService extends ILocationManager.Stub { pw.println(" " + record); } } + pw.println(" Overlay Provider Packages:"); + for (LocationProviderInterface provider : mProviders) { + if (provider instanceof LocationProviderProxy) { + pw.println(" " + provider.getName() + ": " + + ((LocationProviderProxy) provider).getConnectedPackageName()); + } + } pw.println(" Historical Records by Provider:"); for (Map.Entry<PackageProviderKey, PackageStatistics> entry : mRequestStatistics.statistics.entrySet()) { diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index d87991908211..3f97d4fe436e 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -36,7 +36,6 @@ public class SystemServiceManager { private final Context mContext; private boolean mSafeMode; private boolean mRuntimeRestarted; - private boolean mFirstBoot; // Services that should receive lifecycle events. private final ArrayList<SystemService> mServices = new ArrayList<SystemService>(); @@ -260,16 +259,6 @@ public class SystemServiceManager { mRuntimeRestarted = runtimeRestarted; } - /** - * @return true if it's first boot after OTA - */ - public boolean isFirstBoot() { - return mFirstBoot; - } - - void setFirstBoot(boolean firstBoot) { - mFirstBoot = firstBoot; - } /** * Outputs the state of this manager to the System log. diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 4b89b404f9b6..5655dc54ded7 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -349,8 +349,8 @@ public final class ActiveServices { try { // Before going further -- if this app is not allowed to start services in the // background, then at this point we aren't going to let it period. - final int allowed = mAm.checkAllowBackgroundLocked( - r.appInfo.uid, r.packageName, callingPid, false); + final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName, + r.appInfo.targetSdkVersion, callingPid, false, false); if (allowed != ActivityManager.APP_START_MODE_NORMAL) { Slog.w(TAG, "Background start not allowed: service " + service + " to " + r.name.flattenToShortString() @@ -607,8 +607,9 @@ public final class ActiveServices { for (int i=services.mServicesByName.size()-1; i>=0; i--) { ServiceRecord service = services.mServicesByName.valueAt(i); if (service.appInfo.uid == uid && service.startRequested) { - if (mAm.checkAllowBackgroundLocked(service.appInfo.uid, service.packageName, - -1, false) != ActivityManager.APP_START_MODE_NORMAL) { + if (mAm.getAppStartModeLocked(service.appInfo.uid, service.packageName, + service.appInfo.targetSdkVersion, -1, false, false) + != ActivityManager.APP_START_MODE_NORMAL) { if (stopping == null) { stopping = new ArrayList<>(); stopping.add(service); @@ -707,7 +708,7 @@ public final class ActiveServices { try { ServiceRecord r = findServiceLocked(className, token, userId); if (r != null) { - setServiceForegroundInnerLocked(r, userId, notification, flags); + setServiceForegroundInnerLocked(r, id, notification, flags); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 8a0baad3268d..a72950b01dc8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -22,6 +22,7 @@ import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; +import static android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; import static android.app.ActivityManager.RESIZE_MODE_PRESERVE_WINDOW; import static android.app.ActivityManager.StackId.DOCKED_STACK_ID; @@ -4936,7 +4937,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void crashApplication(int uid, int initialPid, String packageName, + public void crashApplication(int uid, int initialPid, String packageName, int userId, String message) { if (checkCallingPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) != PackageManager.PERMISSION_GRANTED) { @@ -4949,7 +4950,7 @@ public class ActivityManagerService extends IActivityManager.Stub } synchronized(this) { - mAppErrors.scheduleAppCrashLocked(uid, initialPid, packageName, message); + mAppErrors.scheduleAppCrashLocked(uid, initialPid, packageName, userId, message); } } @@ -7284,18 +7285,23 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.d(TAG, "tempWhitelistAppForPowerSave(" + callerPid + ", " + callerUid + ", " + targetUid + ", " + duration + ")"); } - synchronized (mPidsSelfLocked) { - final ProcessRecord pr = mPidsSelfLocked.get(callerPid); - if (pr == null) { - Slog.w(TAG, "tempWhitelistAppForPowerSave() no ProcessRecord for pid " + callerPid); - return; - } - if (!pr.whitelistManager) { - if (DEBUG_WHITELISTS) { - Slog.d(TAG, "tempWhitelistAppForPowerSave() for target " + targetUid + ": pid " - + callerPid + " is not allowed"); + + if (checkPermission(CHANGE_DEVICE_IDLE_TEMP_WHITELIST, callerPid, callerUid) + != PackageManager.PERMISSION_GRANTED) { + synchronized (mPidsSelfLocked) { + final ProcessRecord pr = mPidsSelfLocked.get(callerPid); + if (pr == null) { + Slog.w(TAG, "tempWhitelistAppForPowerSave() no ProcessRecord for pid " + + callerPid); + return; + } + if (!pr.whitelistManager) { + if (DEBUG_WHITELISTS) { + Slog.d(TAG, "tempWhitelistAppForPowerSave() for target " + targetUid + + ": pid " + callerPid + " is not allowed"); + } + return; } - return; } } @@ -8020,65 +8026,42 @@ public class ActivityManagerService extends IActivityManager.Stub return readMet && writeMet; } - public int getAppStartMode(int uid, String packageName) { + public boolean isAppStartModeDisabled(int uid, String packageName) { synchronized (this) { - return checkAllowBackgroundLocked(uid, packageName, -1, false); + return getAppStartModeLocked(uid, packageName, 0, -1, false, true) + == ActivityManager.APP_START_MODE_DISABLED; } } // Unified app-op and target sdk check - int appRestrictedInBackgroundLocked(int uid, String packageName) { - if (packageName == null) { - packageName = mPackageManagerInt.getNameForUid(uid); - if (packageName == null) { - Slog.w(TAG, "No package known for uid " + uid); - return ActivityManager.APP_START_MODE_NORMAL; + int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { + // Apps that target O+ are always subject to background check + if (mEnforceBackgroundCheck && packageTargetSdk >= Build.VERSION_CODES.O) { + if (DEBUG_BACKGROUND_CHECK) { + Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted"); } + return ActivityManager.APP_START_MODE_DELAYED_RIGID; } - - // !!! TODO: cache the package/versionCode lookups to fast path this - ApplicationInfo app = getPackageManagerInternalLocked().getApplicationInfo(packageName, - UserHandle.getUserId(uid)); - if (app != null) { - // Apps that target O+ are always subject to background check - if (mEnforceBackgroundCheck && app.targetSdkVersion >= Build.VERSION_CODES.O) { - if (DEBUG_BACKGROUND_CHECK) { - Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted"); - } + // ...and legacy apps get an AppOp check + int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND, + uid, packageName); + if (DEBUG_BACKGROUND_CHECK) { + Slog.i(TAG, "Legacy app " + uid + "/" + packageName + " bg appop " + appop); + } + switch (appop) { + case AppOpsManager.MODE_ALLOWED: + return ActivityManager.APP_START_MODE_NORMAL; + case AppOpsManager.MODE_IGNORED: + return ActivityManager.APP_START_MODE_DELAYED; + default: return ActivityManager.APP_START_MODE_DELAYED_RIGID; - } - // ...and legacy apps get an AppOp check - int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND, - uid, packageName); - if (DEBUG_BACKGROUND_CHECK) { - Slog.i(TAG, "Legacy app " + uid + "/" + packageName + " bg appop " + appop); - } - switch (appop) { - case AppOpsManager.MODE_ALLOWED: - return ActivityManager.APP_START_MODE_NORMAL; - case AppOpsManager.MODE_IGNORED: - return ActivityManager.APP_START_MODE_DELAYED; - default: - return ActivityManager.APP_START_MODE_DELAYED_RIGID; - } - } else { - Slog.w(TAG, "Unknown app " + packageName + " / " + uid); } - return ActivityManager.APP_START_MODE_NORMAL; } // Service launch is available to apps with run-in-background exemptions but // some other background operations are not. If we're doing a check // of service-launch policy, allow those callers to proceed unrestricted. - int appServicesRestrictedInBackgroundLocked(int uid, String packageName) { - if (packageName == null) { - packageName = mPackageManagerInt.getNameForUid(uid); - if (packageName == null) { - Slog.w(TAG, "No package known for uid " + uid); - return ActivityManager.APP_START_MODE_NORMAL; - } - } - + int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { // Persistent app? NB: expects that persistent uids are always active. final UidRecord uidRec = mActiveUids.get(uid); if (uidRec != null && uidRec.persistent) { @@ -8108,11 +8091,11 @@ public class ActivityManagerService extends IActivityManager.Stub } // None of the service-policy criteria apply, so we apply the common criteria - return appRestrictedInBackgroundLocked(uid, packageName); + return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); } - int checkAllowBackgroundLocked(int uid, String packageName, int callingPid, - boolean alwaysRestrict) { + int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk, + int callingPid, boolean alwaysRestrict, boolean disabledOnly) { UidRecord uidRec = mActiveUids.get(uid); if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg=" + packageName + " rec=" + uidRec + " always=" + alwaysRestrict + " idle=" @@ -8130,27 +8113,37 @@ public class ActivityManagerService extends IActivityManager.Stub // We are hard-core about ephemeral apps not running in the background. return ActivityManager.APP_START_MODE_DISABLED; } else { - /** Don't want to allow this exception in the final background check impl? - if (callingPid >= 0) { - ProcessRecord proc; - synchronized (mPidsSelfLocked) { - proc = mPidsSelfLocked.get(callingPid); - } - if (proc != null && proc.curProcState - < ActivityManager.PROCESS_STATE_RECEIVER) { - // Whoever is instigating this is in the foreground, so we will allow it - // to go through. - return ActivityManager.APP_START_MODE_NORMAL; - } + if (disabledOnly) { + // The caller is only interested in whether app starts are completely + // disabled for the given package (that is, it is an instant app). So + // we don't need to go further, which is all just seeing if we should + // apply a "delayed" mode for a regular app. + return ActivityManager.APP_START_MODE_NORMAL; } - */ - final int startMode = (alwaysRestrict) - ? appRestrictedInBackgroundLocked(uid, packageName) - : appServicesRestrictedInBackgroundLocked(uid, packageName); + ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk) + : appServicesRestrictedInBackgroundLocked(uid, packageName, + packageTargetSdk); if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg=" + packageName + " startMode=" + startMode + " onwhitelist=" + isOnDeviceIdleWhitelistLocked(uid)); + if (startMode == ActivityManager.APP_START_MODE_DELAYED) { + // This is an old app that has been forced into a "compatible as possible" + // mode of background check. To increase compatibility, we will allow other + // foreground apps to cause its services to start. + if (callingPid >= 0) { + ProcessRecord proc; + synchronized (mPidsSelfLocked) { + proc = mPidsSelfLocked.get(callingPid); + } + if (proc != null && proc.curProcState + < ActivityManager.PROCESS_STATE_RECEIVER) { + // Whoever is instigating this is in the foreground, so we will allow it + // to go through. + return ActivityManager.APP_START_MODE_NORMAL; + } + } + } return startMode; } } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 202868a3da42..1a2a31b5885c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -167,6 +167,8 @@ final class ActivityManagerShellCommand extends ShellCommand { return runBugReport(pw); case "force-stop": return runForceStop(pw); + case "crash": + return runCrash(pw); case "kill": return runKill(pw); case "kill-all": @@ -851,6 +853,32 @@ final class ActivityManagerShellCommand extends ShellCommand { return 0; } + int runCrash(PrintWriter pw) throws RemoteException { + int userId = UserHandle.USER_ALL; + + String opt; + while ((opt=getNextOption()) != null) { + if (opt.equals("--user")) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } else { + getErrPrintWriter().println("Error: Unknown option: " + opt); + return -1; + } + } + + int pid = -1; + String packageName = null; + final String arg = getNextArgRequired(); + // The argument is either a pid or a package name + try { + pid = Integer.parseInt(arg); + } catch (NumberFormatException e) { + packageName = arg; + } + mInterface.crashApplication(-1, pid, packageName, userId, "shell-induced crash"); + return 0; + } + int runKill(PrintWriter pw) throws RemoteException { int userId = UserHandle.USER_ALL; @@ -2480,6 +2508,8 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" --telephony: will dump only telephony sections."); pw.println(" force-stop [--user <USER_ID> | all | current] <PACKAGE>"); pw.println(" Completely stop the given application package."); + pw.println(" crash [--user <USER_ID>] <PACKAGE|PID>"); + pw.println(" Induce a VM crash in the specified package or process"); pw.println(" kill [--user <USER_ID> | all | current] <PACKAGE>"); pw.println(" Kill all processes associated with the given application."); pw.println(" kill-all"); diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 739a8c464556..384f2f8662b8 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -259,7 +259,16 @@ class AppErrors { } } - void scheduleAppCrashLocked(int uid, int initialPid, String packageName, + /** + * Induce a crash in the given app. + * + * @param uid if nonnegative, the required matching uid of the target to crash + * @param initialPid fast-path match for the target to crash + * @param packageName fallback match if the stated pid is not found or doesn't match uid + * @param userId If nonnegative, required to identify a match by package name + * @param message + */ + void scheduleAppCrashLocked(int uid, int initialPid, String packageName, int userId, String message) { ProcessRecord proc = null; @@ -270,14 +279,15 @@ class AppErrors { synchronized (mService.mPidsSelfLocked) { for (int i=0; i<mService.mPidsSelfLocked.size(); i++) { ProcessRecord p = mService.mPidsSelfLocked.valueAt(i); - if (p.uid != uid) { + if (uid >= 0 && p.uid != uid) { continue; } if (p.pid == initialPid) { proc = p; break; } - if (p.pkgList.containsKey(packageName)) { + if (p.pkgList.containsKey(packageName) + && (userId < 0 || p.userId == userId)) { proc = p; } } @@ -286,7 +296,8 @@ class AppErrors { if (proc == null) { Slog.w(TAG, "crashApplication: nothing for uid=" + uid + " initialPid=" + initialPid - + " packageName=" + packageName); + + " packageName=" + packageName + + " userId=" + userId); return; } diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 61e555bd8b21..ee2467a925c1 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -592,22 +592,6 @@ public final class BroadcastQueue { + " (uid " + r.callingUid + ")"); skip = true; } - if (!skip) { - final int allowed = mService.checkAllowBackgroundLocked(filter.receiverList.uid, - filter.packageName, -1, false); - if (false && allowed == ActivityManager.APP_START_MODE_DISABLED) { - // XXX should we really not allow this? It means that while we are - // keeping an ephemeral app cached, its registered receivers will stop - // receiving broadcasts after it goes idle... so if it comes back to - // the foreground, it won't know what the current state of those broadcasts is. - Slog.w(TAG, "Background execution not allowed: receiving " - + r.intent - + " to " + filter.receiverList.app - + " (pid=" + filter.receiverList.pid - + ", uid=" + filter.receiverList.uid + ")"); - skip = true; - } - } if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid, r.callingPid, r.resolvedType, filter.receiverList.uid)) { @@ -1156,13 +1140,14 @@ public final class BroadcastQueue { info.activityInfo.applicationInfo.uid, false); if (!skip) { - final int allowed = mService.checkAllowBackgroundLocked( - info.activityInfo.applicationInfo.uid, info.activityInfo.packageName, -1, true); + final int allowed = mService.getAppStartModeLocked( + info.activityInfo.applicationInfo.uid, info.activityInfo.packageName, + info.activityInfo.applicationInfo.targetSdkVersion, -1, true, false); if (allowed != ActivityManager.APP_START_MODE_NORMAL) { // We won't allow this receiver to be launched if the app has been // completely disabled from launches, or it was not explicitly sent // to it and the app is in a state that should not receive it - // (depending on how checkAllowBackgroundLocked has determined that). + // (depending on how getAppStartModeLocked has determined that). if (allowed == ActivityManager.APP_START_MODE_DISABLED) { Slog.w(TAG, "Background execution disabled: receiving " + r.intent + " to " diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 71c7fd348fb3..82b00da5b809 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -534,7 +534,7 @@ final class ServiceRecord extends Binder { // get to be foreground. ams.setServiceForeground(name, ServiceRecord.this, 0, null, 0); - ams.crashApplication(appUid, appPid, localPackageName, + ams.crashApplication(appUid, appPid, localPackageName, -1, "Bad notification for startForeground: " + e); } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 5bf92d7e3322..728476aebf3e 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -47,6 +47,7 @@ import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKING; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.Dialog; import android.app.IStopUserCallback; @@ -55,6 +56,7 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.IIntentReceiver; import android.content.Intent; +import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.BatteryStats; @@ -255,7 +257,9 @@ final class UserController { // storage is already unlocked. if (uss.setState(STATE_BOOTING, STATE_RUNNING_LOCKED)) { mInjector.getUserManagerInternal().setUserState(userId, uss.state); - if (!mInjector.isRuntimeRestarted() && !mInjector.isFirstBoot()) { + // Do not report secondary users, runtime restarts or first boot/upgrade + if (userId == UserHandle.USER_SYSTEM + && !mInjector.isRuntimeRestarted() && !mInjector.isFirstBootOrUpgrade()) { int uptimeSeconds = (int)(SystemClock.elapsedRealtime() / 1000); MetricsLogger.histogram(mInjector.getContext(), "framework_locked_boot_completed", uptimeSeconds); @@ -436,7 +440,9 @@ final class UserController { } Slog.d(TAG, "Sending BOOT_COMPLETE user #" + userId); - if (!mInjector.isRuntimeRestarted() && !mInjector.isFirstBoot()) { + // Do not report secondary users, runtime restarts or first boot/upgrade + if (userId == UserHandle.USER_SYSTEM + && !mInjector.isRuntimeRestarted() && !mInjector.isFirstBootOrUpgrade()) { int uptimeSeconds = (int) (SystemClock.elapsedRealtime() / 1000); MetricsLogger.histogram(mInjector.getContext(), "framework_boot_completed", uptimeSeconds); @@ -1709,8 +1715,13 @@ final class UserController { return mService.mSystemServiceManager.isRuntimeRestarted(); } - boolean isFirstBoot() { - return mService.mSystemServiceManager.isFirstBoot(); + boolean isFirstBootOrUpgrade() { + IPackageManager pm = AppGlobals.getPackageManager(); + try { + return pm.isFirstBoot() || pm.isUpgrade(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } void sendPreBootBroadcast(int userId, boolean quiet, final Runnable onFinish) { diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 11a3f1189129..5b539ff1976d 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -1019,8 +1019,7 @@ public class SyncManager { final int owningUid = syncAdapterInfo.uid; final String owningPackage = syncAdapterInfo.componentName.getPackageName(); try { - if (ActivityManager.getService().getAppStartMode(owningUid, - owningPackage) == ActivityManager.APP_START_MODE_DISABLED) { + if (ActivityManager.getService().isAppStartModeDisabled(owningUid, owningPackage)) { Slog.w(TAG, "Not scheduling job " + syncAdapterInfo.uid + ":" + syncAdapterInfo.componentName + " -- package not allowed to start"); diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 867322578a66..841a951923a3 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -285,6 +285,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { int activeColorMode) { List<Integer> pendingColorModes = new ArrayList<>(); + if (colorModes == null) return false; // Build an updated list of all existing color modes. boolean colorModesAdded = false; for (int colorMode: colorModes) { diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index f42c5be0ebf9..a7480133972a 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -566,8 +566,8 @@ public final class JobSchedulerService extends com.android.server.SystemService String tag) { JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag); try { - if (ActivityManager.getService().getAppStartMode(uId, - job.getService().getPackageName()) == ActivityManager.APP_START_MODE_DISABLED) { + if (ActivityManager.getService().isAppStartModeDisabled(uId, + job.getService().getPackageName())) { Slog.w(TAG, "Not scheduling job " + uId + ":" + job.toString() + " -- package not allowed to start"); return JobScheduler.RESULT_FAILURE; @@ -1201,9 +1201,8 @@ public final class JobSchedulerService extends com.android.server.SystemService public void process(JobStatus job) { if (isReadyToBeExecutedLocked(job)) { try { - if (ActivityManager.getService().getAppStartMode(job.getUid(), - job.getJob().getService().getPackageName()) - == ActivityManager.APP_START_MODE_DISABLED) { + if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(), + job.getJob().getService().getPackageName())) { Slog.w(TAG, "Aborting job " + job.getUid() + ":" + job.getJob().toString() + " -- package not allowed to start"); mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6c66a60d530f..168884dbe189 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -198,6 +198,8 @@ public class NotificationManagerService extends SystemService { static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; + static final long SNOOZE_UNTIL_UNSPECIFIED = -1; + static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION; @@ -229,6 +231,7 @@ public class NotificationManagerService extends SystemService { private IActivityManager mAm; private IPackageManager mPackageManager; + private PackageManager mPackageManagerClient; AudioManager mAudioManager; AudioManagerInternal mAudioManagerInternal; @Nullable StatusBarManagerInternal mStatusBar; @@ -268,6 +271,7 @@ public class NotificationManagerService extends SystemService { private boolean mNotificationPulseEnabled; // used as a mutex for access to all active notifications & listeners + final Object mNotificationLock = new Object(); final ArrayList<NotificationRecord> mNotificationList = new ArrayList<NotificationRecord>(); final ArrayMap<String, NotificationRecord> mNotificationsByKey = @@ -372,7 +376,7 @@ public class NotificationManagerService extends SystemService { private void loadPolicyFile() { if (DBG) Slog.d(TAG, "loadPolicyFile"); - synchronized(mPolicyFile) { + synchronized (mPolicyFile) { FileInputStream infile = null; try { @@ -491,7 +495,7 @@ public class NotificationManagerService extends SystemService { @Override public void onSetDisabled(int status) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mDisableNotificationEffects = (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; if (disableNotificationEffects(null) != null) { @@ -519,7 +523,7 @@ public class NotificationManagerService extends SystemService { @Override public void onClearAll(int callingUid, int callingPid, int userId) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { cancelAllLocked(callingUid, callingPid, userId, REASON_DELEGATE_CANCEL_ALL, null, /*includeCurrentProfiles*/ true); } @@ -527,7 +531,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationClick(int callingUid, int callingPid, String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r == null) { Log.w(TAG, "No notification with key: " + key); @@ -548,7 +552,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationActionClick(int callingUid, int callingPid, String key, int actionIndex) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r == null) { Log.w(TAG, "No notification with key: " + key); @@ -584,7 +588,7 @@ public class NotificationManagerService extends SystemService { @Override public void clearEffects() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (DBG) Slog.d(TAG, "clearEffects"); clearSoundLocked(); clearVibrateLocked(); @@ -601,7 +605,7 @@ public class NotificationManagerService extends SystemService { REASON_DELEGATE_ERROR, null); long ident = Binder.clearCallingIdentity(); try { - ActivityManager.getService().crashApplication(uid, initialPid, pkg, + ActivityManager.getService().crashApplication(uid, initialPid, pkg, -1, "Bad notification posted from package " + pkg + ": " + message); } catch (RemoteException e) { @@ -612,7 +616,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationVisibilityChanged(NotificationVisibility[] newlyVisibleKeys, NotificationVisibility[] noLongerVisibleKeys) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { for (NotificationVisibility nv : newlyVisibleKeys) { NotificationRecord r = mNotificationsByKey.get(nv.key); if (r == null) continue; @@ -635,7 +639,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { r.stats.onExpansionChanged(userAction, expanded); @@ -949,7 +953,8 @@ public class NotificationManagerService extends SystemService { // TODO: Tests should call onStart instead once the methods above are removed. @VisibleForTesting - void init(IPackageManager packageManager, LightsManager lightsManager) { + void init(Looper looper, IPackageManager packageManager, PackageManager packageManagerClient, + LightsManager lightsManager, NotificationListeners notificationListeners) { Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -957,11 +962,12 @@ public class NotificationManagerService extends SystemService { mAm = ActivityManager.getService(); mPackageManager = packageManager; + mPackageManagerClient = packageManagerClient; mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); - mHandler = new WorkerHandler(); + mHandler = new WorkerHandler(looper); mRankingThread.start(); String[] extractorNames; try { @@ -991,7 +997,7 @@ public class NotificationManagerService extends SystemService { new Intent(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT), UserHandle.ALL, android.Manifest.permission.MANAGE_NOTIFICATIONS); - synchronized(mNotificationList) { + synchronized (mNotificationLock) { updateInterruptionFilterLocked(); } } @@ -1019,7 +1025,7 @@ public class NotificationManagerService extends SystemService { mGroupHelper = new GroupHelper(new GroupHelper.Callback() { @Override public void addAutoGroup(String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { addAutogroupKeyLocked(key); } mRankingHandler.requestSort(false); @@ -1027,7 +1033,7 @@ public class NotificationManagerService extends SystemService { @Override public void removeAutoGroup(String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { removeAutogroupKeyLocked(key); } mRankingHandler.requestSort(false); @@ -1040,7 +1046,7 @@ public class NotificationManagerService extends SystemService { @Override public void removeAutoGroupSummary(int userId, String pkg) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { clearAutogroupSummaryLocked(userId, pkg); } } @@ -1051,8 +1057,8 @@ public class NotificationManagerService extends SystemService { syncBlockDb(); - // This is a MangedServices object that keeps track of the listeners. - mListeners = new NotificationListeners(); + // This is a ManagedServices object that keeps track of the listeners. + mListeners = notificationListeners; // This is a MangedServices object that keeps track of the assistant. mNotificationAssistants = new NotificationAssistants(); @@ -1134,7 +1140,8 @@ public class NotificationManagerService extends SystemService { @Override public void onStart() { - init(AppGlobals.getPackageManager(), getLocalService(LightsManager.class)); + init(Looper.myLooper(), AppGlobals.getPackageManager(), getContext().getPackageManager(), + getLocalService(LightsManager.class), new NotificationListeners()); publishBinderService(Context.NOTIFICATION_SERVICE, mService); publishLocalService(NotificationManagerInternal.class, mInternalService); } @@ -1627,7 +1634,7 @@ public class NotificationManagerService extends SystemService { // noteOp will check to make sure the callingPkg matches the uid if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg) == AppOpsManager.MODE_ALLOWED) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { tmp = new StatusBarNotification[mNotificationList.size()]; final int N = mNotificationList.size(); for (int i=0; i<N; i++) { @@ -1653,7 +1660,7 @@ public class NotificationManagerService extends SystemService { final ArrayMap<String, StatusBarNotification> map = new ArrayMap<>(mNotificationList.size() + mEnqueuedNotifications.size()); - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { StatusBarNotification sbn = sanitizeSbn(pkg, userId, @@ -1668,10 +1675,8 @@ public class NotificationManagerService extends SystemService { map.put(sbn.getKey(), sbn); } } - } - synchronized (mEnqueuedNotifications) { - final int N = mEnqueuedNotifications.size(); - for (int i = 0; i < N; i++) { + final int M = mEnqueuedNotifications.size(); + for (int i = 0; i < M; i++) { StatusBarNotification sbn = sanitizeSbn(pkg, userId, mEnqueuedNotifications.get(i).sbn); if (sbn != null) { @@ -1763,7 +1768,7 @@ public class NotificationManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (keys != null) { final int N = keys.length; @@ -1826,7 +1831,7 @@ public class NotificationManagerService extends SystemService { public void setNotificationsShownFromListener(INotificationListener token, String[] keys) { long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (keys != null) { final int N = keys.length; @@ -1881,7 +1886,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, snoozeCriterionId, info); + snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, snoozeCriterionId, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1898,7 +1903,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, snoozeUntil, info); + snoozeNotificationInt(key, snoozeUntil, null, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1914,7 +1919,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, info); + snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, null, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1950,7 +1955,7 @@ public class NotificationManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (info.supportsProfiles()) { Log.e(TAG, "Ignoring deprecated cancelNotification(pkg, tag, id) " @@ -1979,7 +1984,7 @@ public class NotificationManagerService extends SystemService { @Override public ParceledListSlice<StatusBarNotification> getActiveNotificationsFromListener( INotificationListener token, String[] keys, int trim) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); final boolean getKeys = keys != null; final int N = getKeys ? keys.length : mNotificationList.size(); @@ -2004,7 +2009,7 @@ public class NotificationManagerService extends SystemService { public void requestHintsFromListener(INotificationListener token, int hints) { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); final int disableEffectsMask = HINT_HOST_DISABLE_EFFECTS | HINT_HOST_DISABLE_NOTIFICATION_EFFECTS @@ -2025,7 +2030,7 @@ public class NotificationManagerService extends SystemService { @Override public int getHintsFromListener(INotificationListener token) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { return mListenerHints; } } @@ -2035,7 +2040,7 @@ public class NotificationManagerService extends SystemService { int interruptionFilter) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); mZenModeHelper.requestFromListener(info.component, interruptionFilter); updateInterruptionFilterLocked(); @@ -2056,7 +2061,7 @@ public class NotificationManagerService extends SystemService { @Override public void setOnNotificationPostedTrimFromListener(INotificationListener token, int trim) throws RemoteException { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (info == null) return; mListeners.setOnNotificationPostedTrimLocked(info, trim); @@ -2375,7 +2380,7 @@ public class NotificationManagerService extends SystemService { enforceSystemOrSystemUI("grant notification policy access"); final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mPolicyAccess.put(pkg, granted); } } finally { @@ -2410,7 +2415,7 @@ public class NotificationManagerService extends SystemService { Adjustment adjustment) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { @@ -2432,7 +2437,7 @@ public class NotificationManagerService extends SystemService { Adjustment adjustment) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); NotificationRecord n = mNotificationsByKey.get(adjustment.getKey()); applyAdjustment(n, adjustment); @@ -2449,7 +2454,7 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); for (Adjustment adjustment : adjustments) { NotificationRecord n = mNotificationsByKey.get(adjustment.getKey()); @@ -2555,9 +2560,8 @@ public class NotificationManagerService extends SystemService { ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId); if (summaries != null && summaries.containsKey(pkg)) { // Clear summary. - final NotificationRecord removed = mNotificationsByKey.get(summaries.remove(pkg)); + final NotificationRecord removed = findNotificationByKeyLocked(summaries.remove(pkg)); if (removed != null) { - mNotificationList.remove(removed); cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED); } } @@ -2566,7 +2570,7 @@ public class NotificationManagerService extends SystemService { // Posts a 'fake' summary for a package that has exceeded the solo-notification limit. private void createAutoGroupSummary(int userId, String pkg, String triggeringKey) { NotificationRecord summaryRecord = null; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord notificationRecord = mNotificationsByKey.get(triggeringKey); if (notificationRecord == null) { // The notification could have been cancelled again already. A successive @@ -2617,7 +2621,7 @@ public class NotificationManagerService extends SystemService { } } if (summaryRecord != null) { - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mEnqueuedNotifications.add(summaryRecord); } mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord)); @@ -2672,7 +2676,7 @@ public class NotificationManagerService extends SystemService { } } - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (!zenOnly) { N = mNotificationList.size(); if (N > 0) { @@ -2710,19 +2714,17 @@ public class NotificationManagerService extends SystemService { } pw.println(" mArchive=" + mArchive.toString()); Iterator<StatusBarNotification> iter = mArchive.descendingIterator(); - int i=0; + int j=0; while (iter.hasNext()) { final StatusBarNotification sbn = iter.next(); if (filter != null && !filter.matches(sbn)) continue; pw.println(" " + sbn); - if (++i >= 5) { + if (++j >= 5) { if (iter.hasNext()) pw.println(" ..."); break; } } - } - synchronized (mEnqueuedNotifications) { if (!zenOnly) { N = mEnqueuedNotifications.size(); if (N > 0) { @@ -2817,14 +2819,14 @@ public class NotificationManagerService extends SystemService { public void removeForegroundServiceFlagFromNotification(String pkg, int notificationId, int userId) { checkCallerIsSystem(); - synchronized (mNotificationList) { - int i = indexOfNotificationLocked(pkg, null, notificationId, userId); - if (i < 0) { + synchronized (mNotificationLock) { + NotificationRecord r = findNotificationByListLocked(mNotificationList, pkg, null, + notificationId, userId); + if (r == null) { Log.d(TAG, "stripForegroundServiceFlag: Could not find notification with " + "pkg=" + pkg + " / id=" + notificationId + " / userId=" + userId); return; } - NotificationRecord r = mNotificationList.get(i); StatusBarNotification sbn = r.sbn; // NoMan adds flags FLAG_NO_CLEAR and FLAG_ONGOING_EVENT when it sees // FLAG_FOREGROUND_SERVICE. Hence it's not enough to remove FLAG_FOREGROUND_SERVICE, @@ -2861,7 +2863,7 @@ public class NotificationManagerService extends SystemService { // Fix the notification as best we can. try { - final ApplicationInfo ai = getContext().getPackageManager().getApplicationInfoAsUser( + final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser( pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, (userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, userId, notification); @@ -2886,8 +2888,8 @@ public class NotificationManagerService extends SystemService { // Limit the number of notifications that any given package except the android // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { - synchronized (mNotificationList) { - if(mNotificationsByKey.get(n.getKey()) != null) { + synchronized (mNotificationLock) { + if (mNotificationsByKey.get(n.getKey()) != null) { // this is an update, rate limit updates only final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg); if (appEnqueueRate > mMaxPackageEnqueueRate) { @@ -2945,7 +2947,7 @@ public class NotificationManagerService extends SystemService { // setup local book-keeping final NotificationRecord r = new NotificationRecord(getContext(), n); - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mEnqueuedNotifications.add(r); } mHandler.post(new EnqueueNotificationRunnable(userId, r)); @@ -2964,7 +2966,7 @@ public class NotificationManagerService extends SystemService { @Override public void run() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (mSnoozeHelper.isSnoozed(userId, r.sbn.getPackageName(), r.getKey())) { // TODO: log to event log if (DBG) { @@ -3059,9 +3061,9 @@ public class NotificationManagerService extends SystemService { @Override public void run() { - try { - NotificationRecord r = null; - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { + try { + NotificationRecord r = null; int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); @@ -3070,12 +3072,10 @@ public class NotificationManagerService extends SystemService { break; } } - } - if (r == null) { - Slog.e(TAG, "Cannot find enqueued record for key: " + key); - return; - } - synchronized (mNotificationList) { + if (r == null) { + Slog.i(TAG, "Cannot find enqueued record for key: " + key); + return; + } NotificationRecord old = mNotificationsByKey.get(key); final StatusBarNotification n = r.sbn; final Notification notification = n.getNotification(); @@ -3134,9 +3134,7 @@ public class NotificationManagerService extends SystemService { } buzzBeepBlinkLocked(r); - } - } finally { - synchronized (mEnqueuedNotifications) { + } finally { int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); @@ -3193,8 +3191,7 @@ public class NotificationManagerService extends SystemService { // notification was a summary and the new one isn't, or when the old // notification was a summary and its group key changed. if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) { - cancelGroupChildrenLocked(old, callingUid, callingPid, null, - REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); + cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */); } } @@ -3463,7 +3460,7 @@ public class NotificationManagerService extends SystemService { RankingReconsideration recon = (RankingReconsideration) message.obj; recon.run(); boolean changed; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final NotificationRecord record = mNotificationsByKey.get(recon.getKey()); if (record == null) { return; @@ -3491,7 +3488,7 @@ public class NotificationManagerService extends SystemService { private void handleRankingSort(Message msg) { if (!(msg.obj instanceof Boolean)) return; boolean forceUpdate = ((Boolean) msg.obj == null) ? false : (boolean) msg.obj; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final int N = mNotificationList.size(); ArrayList<String> orderBefore = new ArrayList<String>(N); ArrayList<String> groupOverrideBefore = new ArrayList<>(N); @@ -3548,7 +3545,7 @@ public class NotificationManagerService extends SystemService { } private void handleSendRankingUpdate() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyRankingUpdateLocked(); } } @@ -3567,19 +3564,23 @@ public class NotificationManagerService extends SystemService { } private void handleListenerHintsChanged(int hints) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyListenerHintsChangedLocked(hints); } } private void handleListenerInterruptionFilterChanged(int interruptionFilter) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyInterruptionFilterChanged(interruptionFilter); } } private final class WorkerHandler extends Handler { + public WorkerHandler(Looper looper) { + super(looper); + } + @Override public void handleMessage(Message msg) { @@ -3665,6 +3666,17 @@ public class NotificationManagerService extends SystemService { } private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) { + final String canceledKey = r.getKey(); + + // Remove from either list + boolean wasPosted; + if (mNotificationList.remove(r)) { + mNotificationsByKey.remove(r.sbn.getKey()); + wasPosted = true; + } else { + mEnqueuedNotifications.remove(r); + wasPosted = false; + } // Record caller. recordCallerLocked(r); @@ -3682,49 +3694,50 @@ public class NotificationManagerService extends SystemService { } } - // status bar - if (r.getNotification().getSmallIcon() != null) { - r.isCanceled = true; - mListeners.notifyRemovedLocked(r.sbn, reason); - mHandler.post(new Runnable() { - @Override - public void run() { - mGroupHelper.onNotificationRemoved(r.sbn); - } - }); - } - - final String canceledKey = r.getKey(); + // Only cancel these if this notification actually got to be posted. + if (wasPosted) { + // status bar + if (r.getNotification().getSmallIcon() != null) { + r.isCanceled = true; + mListeners.notifyRemovedLocked(r.sbn, reason); + mHandler.post(new Runnable() { + @Override + public void run() { + mGroupHelper.onNotificationRemoved(r.sbn); + } + }); + } - // sound - if (canceledKey.equals(mSoundNotificationKey)) { - mSoundNotificationKey = null; - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - player.stopAsync(); + // sound + if (canceledKey.equals(mSoundNotificationKey)) { + mSoundNotificationKey = null; + final long identity = Binder.clearCallingIdentity(); + try { + final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); + if (player != null) { + player.stopAsync(); + } + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(identity); } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); } - } - // vibrate - if (canceledKey.equals(mVibrateNotificationKey)) { - mVibrateNotificationKey = null; - long identity = Binder.clearCallingIdentity(); - try { - mVibrator.cancel(); - } - finally { - Binder.restoreCallingIdentity(identity); + // vibrate + if (canceledKey.equals(mVibrateNotificationKey)) { + mVibrateNotificationKey = null; + long identity = Binder.clearCallingIdentity(); + try { + mVibrator.cancel(); + } + finally { + Binder.restoreCallingIdentity(identity); + } } - } - // light - mLights.remove(canceledKey); + // light + mLights.remove(canceledKey); + } // Record usage stats // TODO: add unbundling stats? @@ -3741,10 +3754,9 @@ public class NotificationManagerService extends SystemService { break; } - mNotificationsByKey.remove(r.sbn.getKey()); String groupKey = r.getGroupKey(); NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey); - if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) { + if (groupSummary != null && groupSummary.getKey().equals(canceledKey)) { mSummaryByGroupKey.remove(groupKey); } final ArrayMap<String, String> summaries = mAutobundledSummaries.get(r.sbn.getUserId()); @@ -3779,10 +3791,11 @@ public class NotificationManagerService extends SystemService { if (DBG) EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, id, tag, userId, mustHaveFlags, mustNotHaveFlags, reason, listenerName); - synchronized (mNotificationList) { - int index = indexOfNotificationLocked(pkg, tag, id, userId); - if (index >= 0) { - NotificationRecord r = mNotificationList.get(index); + synchronized (mNotificationLock) { + // Look for the notification, searching both the posted and enqueued lists. + NotificationRecord r = findNotificationLocked(pkg, tag, id, userId); + if (r != null) { + // The notification was found, check if it should be removed. // Ideally we'd do this in the caller of this method. However, that would // require the caller to also find the notification. @@ -3797,13 +3810,13 @@ public class NotificationManagerService extends SystemService { return; } - mNotificationList.remove(index); - + // Cancel the notification. cancelNotificationLocked(r, sendDelete, reason); cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName, - REASON_GROUP_SUMMARY_CANCELED, sendDelete); + sendDelete); updateLightsLocked(); } else { + // No notification was found, assume that it is snoozed and cancel it. final boolean wasSnoozed = mSnoozeHelper.cancel(userId, pkg, tag, id); if (wasSnoozed) { savePolicyFile(); @@ -3842,120 +3855,131 @@ public class NotificationManagerService extends SystemService { * Cancels all notifications from a given package that have all of the * {@code mustHaveFlags}. */ - boolean cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, + void cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, int mustHaveFlags, int mustNotHaveFlags, boolean doit, int userId, int reason, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - EventLogTags.writeNotificationCancelAll(callingUid, callingPid, - pkg, userId, mustHaveFlags, mustNotHaveFlags, reason, - listenerName); + mHandler.post(new Runnable() { + @Override + public void run() { + String listenerName = listener == null ? null : listener.component.toShortString(); + EventLogTags.writeNotificationCancelAll(callingUid, callingPid, + pkg, userId, mustHaveFlags, mustNotHaveFlags, reason, + listenerName); - synchronized (mNotificationList) { - final int N = mNotificationList.size(); - ArrayList<NotificationRecord> canceledNotifications = null; - for (int i = N-1; i >= 0; --i) { - NotificationRecord r = mNotificationList.get(i); - if (!notificationMatchesUserId(r, userId)) { - continue; - } - // Don't remove notifications to all, if there's no package name specified - if (r.getUserId() == UserHandle.USER_ALL && pkg == null) { - continue; - } - if ((r.getFlags() & mustHaveFlags) != mustHaveFlags) { - continue; - } - if ((r.getFlags() & mustNotHaveFlags) != 0) { - continue; + // Why does this parameter exist? Do we actually want to execute the above if doit + // is false? + if (!doit) { + return; } - if (pkg != null && !r.sbn.getPackageName().equals(pkg)) { - continue; + + synchronized (mNotificationLock) { + FlagChecker flagChecker = (int flags) -> { + if ((flags & mustHaveFlags) != mustHaveFlags) { + return false; + } + if ((flags & mustNotHaveFlags) != 0) { + return false; + } + return true; + }; + + cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, + pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, flagChecker, + false /*includeCurrentProfiles*/, userId, false /*sendDelete*/, reason, + listenerName); + cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, + callingPid, pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, + flagChecker, false /*includeCurrentProfiles*/, userId, + false /*sendDelete*/, reason, listenerName); + mSnoozeHelper.cancel(userId, pkg); } - if (channelId != null && !channelId.equals(r.getChannel().getId())) { + } + }); + } + + private interface FlagChecker { + // Returns false if these flags do not pass the defined flag test. + public boolean apply(int flags); + } + + private void cancelAllNotificationsByListLocked(ArrayList<NotificationRecord> notificationList, + int callingUid, int callingPid, String pkg, boolean nullPkgIndicatesUserSwitch, + String channelId, FlagChecker flagChecker, boolean includeCurrentProfiles, int userId, + boolean sendDelete, int reason, String listenerName) { + ArrayList<NotificationRecord> canceledNotifications = null; + for (int i = notificationList.size() - 1; i >= 0; --i) { + NotificationRecord r = notificationList.get(i); + if (includeCurrentProfiles) { + if (!notificationMatchesCurrentProfiles(r, userId)) { continue; } - if (canceledNotifications == null) { - canceledNotifications = new ArrayList<>(); - } - canceledNotifications.add(r); - if (!doit) { - return true; - } - mNotificationList.remove(i); - cancelNotificationLocked(r, false, reason); + } else if (!notificationMatchesUserId(r, userId)) { + continue; } - mSnoozeHelper.cancel(userId, pkg); - if (doit && canceledNotifications != null) { - final int M = canceledNotifications.size(); - for (int i = 0; i < M; i++) { - cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName, REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); - } + // Don't remove notifications to all, if there's no package name specified + if (nullPkgIndicatesUserSwitch && pkg == null && r.getUserId() == UserHandle.USER_ALL) { + continue; } - if (canceledNotifications != null) { - updateLightsLocked(); + if (!flagChecker.apply(r.getFlags())) { + continue; + } + if (pkg != null && !r.sbn.getPackageName().equals(pkg)) { + continue; + } + if (channelId != null && !channelId.equals(r.getChannel().getId())) { + continue; } - return canceledNotifications != null; - } - } - void snoozeNotificationInt(String key, String snoozeCriterionId, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - // TODO: write to event log - if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %s, %s)", - key, snoozeCriterionId, listenerName)); - } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - mNotificationAssistants.notifyAssistantSnoozedLocked(r.sbn, snoozeCriterionId); - updateLightsLocked(); - mSnoozeHelper.snooze(r); - savePolicyFile(); + if (canceledNotifications == null) { + canceledNotifications = new ArrayList<>(); } + canceledNotifications.add(r); + cancelNotificationLocked(r, sendDelete, reason); + } + if (canceledNotifications != null) { + final int M = canceledNotifications.size(); + for (int i = 0; i < M; i++) { + cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, + listenerName, false /* sendDelete */); + } + updateLightsLocked(); } } - void snoozeNotificationInt(String key, long until, ManagedServiceInfo listener) { + void snoozeNotificationInt(String key, long until, String snoozeCriterionId, + ManagedServiceInfo listener) { String listenerName = listener == null ? null : listener.component.toShortString(); // TODO: write to event log if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %d, %s)", key, until, listenerName)); + Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, until, snoozeCriterionId, + listenerName)); } - if (until < System.currentTimeMillis()) { + if (until != SNOOZE_UNTIL_UNSPECIFIED && until < System.currentTimeMillis()) { return; } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - updateLightsLocked(); - mSnoozeHelper.snooze(r, until); - savePolicyFile(); - } - } - } - - void snoozeNotificationInt(String key, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - // TODO: write to event log - if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %s)", key, listenerName)); - } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - updateLightsLocked(); - mSnoozeHelper.snooze(r); - savePolicyFile(); + // Needs to post so that it can cancel notifications not yet enqueued. + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mNotificationLock) { + final NotificationRecord r = findNotificationByKeyLocked(key); + if (r != null) { + cancelNotificationLocked(r, false, REASON_SNOOZED); + updateLightsLocked(); + if (snoozeCriterionId != null) { + mNotificationAssistants.notifyAssistantSnoozedLocked(r.sbn, + snoozeCriterionId); + } + if (until == SNOOZE_UNTIL_UNSPECIFIED) { + mSnoozeHelper.snooze(r); + } else { + mSnoozeHelper.snooze(r, until); + } + savePolicyFile(); + } + } } - } + }); } void unsnoozeNotificationInt(String key, ManagedServiceInfo listener) { @@ -3970,47 +3994,40 @@ public class NotificationManagerService extends SystemService { void cancelAllLocked(int callingUid, int callingPid, int userId, int reason, ManagedServiceInfo listener, boolean includeCurrentProfiles) { - String listenerName = listener == null ? null : listener.component.toShortString(); - EventLogTags.writeNotificationCancelAll(callingUid, callingPid, - null, userId, 0, 0, reason, listenerName); - - ArrayList<NotificationRecord> canceledNotifications = null; - final int N = mNotificationList.size(); - for (int i=N-1; i>=0; i--) { - NotificationRecord r = mNotificationList.get(i); - if (includeCurrentProfiles) { - if (!notificationMatchesCurrentProfiles(r, userId)) { - continue; - } - } else { - if (!notificationMatchesUserId(r, userId)) { - continue; - } - } - - if ((r.getFlags() & (Notification.FLAG_ONGOING_EVENT - | Notification.FLAG_NO_CLEAR)) == 0) { - mNotificationList.remove(i); - cancelNotificationLocked(r, true, reason); - // Make a note so we can cancel children later. - if (canceledNotifications == null) { - canceledNotifications = new ArrayList<>(); + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mNotificationLock) { + String listenerName = + listener == null ? null : listener.component.toShortString(); + EventLogTags.writeNotificationCancelAll(callingUid, callingPid, + null, userId, 0, 0, reason, listenerName); + + FlagChecker flagChecker = (int flags) -> { + if ((flags & (Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR)) + != 0) { + return false; + } + return true; + }; + + cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, + null, false /*nullPkgIndicatesUserSwitch*/, null, flagChecker, + includeCurrentProfiles, userId, true /*sendDelete*/, reason, + listenerName); + cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, + callingPid, null, false /*nullPkgIndicatesUserSwitch*/, null, + flagChecker, includeCurrentProfiles, userId, true /*sendDelete*/, + reason, listenerName); + mSnoozeHelper.cancel(userId, includeCurrentProfiles); } - canceledNotifications.add(r); } - } - mSnoozeHelper.cancel(userId, includeCurrentProfiles); - int M = canceledNotifications != null ? canceledNotifications.size() : 0; - for (int i = 0; i < M; i++) { - cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName, REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); - } - updateLightsLocked(); + }); } // Warning: The caller is responsible for invoking updateLightsLocked(). private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid, - String listenerName, int reason, boolean sendDelete) { + String listenerName, boolean sendDelete) { Notification n = r.getNotification(); if (!n.isGroupSummary()) { return; @@ -4024,16 +4041,26 @@ public class NotificationManagerService extends SystemService { return; } - final int N = mNotificationList.size(); - for (int i = N - 1; i >= 0; i--) { - NotificationRecord childR = mNotificationList.get(i); - StatusBarNotification childSbn = childR.sbn; + cancelGroupChildrenByListLocked(mNotificationList, r, callingUid, callingPid, listenerName, + sendDelete); + cancelGroupChildrenByListLocked(mEnqueuedNotifications, r, callingUid, callingPid, + listenerName, sendDelete); + } + + private void cancelGroupChildrenByListLocked(ArrayList<NotificationRecord> notificationList, + NotificationRecord parentNotification, int callingUid, int callingPid, + String listenerName, boolean sendDelete) { + final String pkg = parentNotification.sbn.getPackageName(); + final int userId = parentNotification.getUserId(); + final int reason = REASON_GROUP_SUMMARY_CANCELED; + for (int i = notificationList.size() - 1; i >= 0; i--) { + final NotificationRecord childR = notificationList.get(i); + final StatusBarNotification childSbn = childR.sbn; if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) && - childR.getGroupKey().equals(r.getGroupKey()) + childR.getGroupKey().equals(parentNotification.getGroupKey()) && (childR.getFlags() & Notification.FLAG_FOREGROUND_SERVICE) == 0) { EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0, reason, listenerName); - mNotificationList.remove(i); cancelNotificationLocked(childR, sendDelete, reason); } } @@ -4082,19 +4109,49 @@ public class NotificationManagerService extends SystemService { } } - // lock on mNotificationList - int indexOfNotificationLocked(String pkg, String tag, int id, int userId) + // Searches both enqueued and posted notifications by key. + // TODO: need to combine a bunch of these getters with slightly different behavior. + // TODO: Should enqueuing just add to mNotificationsByKey instead? + private NotificationRecord findNotificationByKeyLocked(String key) { + final int N = mNotificationList.size(); + for (int i = 0; i < N; i++) { + if (key.equals(mNotificationList.get(i).getKey())) { + return mNotificationList.get(i); + } + } + final int M = mEnqueuedNotifications.size(); + for (int i = 0; i < M; i++) { + if (key.equals(mEnqueuedNotifications.get(i).getKey())) { + return mEnqueuedNotifications.get(i); + } + } + return null; + } + + private NotificationRecord findNotificationLocked(String pkg, String tag, int id, int userId) { + NotificationRecord r; + if ((r = findNotificationByListLocked(mNotificationList, pkg, tag, id, userId)) != null) { + return r; + } + if ((r = findNotificationByListLocked(mEnqueuedNotifications, pkg, tag, id, userId)) + != null) { + return r; + } + return null; + } + + private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list, + String pkg, String tag, int id, int userId) { - ArrayList<NotificationRecord> list = mNotificationList; final int len = list.size(); - for (int i=0; i<len; i++) { + for (int i = 0; i < len; i++) { NotificationRecord r = list.get(i); if (notificationMatchesUserId(r, userId) && r.sbn.getId() == id && TextUtils.equals(r.sbn.getTag(), tag) && r.sbn.getPackageName().equals(pkg)) { - return i; + return r; } } - return -1; + return null; } // lock on mNotificationList @@ -4109,7 +4166,7 @@ public class NotificationManagerService extends SystemService { } private void updateNotificationPulse() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { updateLightsLocked(); } } @@ -4413,7 +4470,7 @@ public class NotificationManagerService extends SystemService { public void onServiceAdded(ManagedServiceInfo info) { final INotificationListener listener = (INotificationListener) info.service; final NotificationRankingUpdate update; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { update = makeRankingUpdateLocked(info); } try { @@ -4608,12 +4665,12 @@ public class NotificationManagerService extends SystemService { } } - private boolean isListenerPackage(String packageName) { + public boolean isListenerPackage(String packageName) { if (packageName == null) { return false; } // TODO: clean up locking object later - synchronized (mNotificationList) { + synchronized (mNotificationLock) { for (final ManagedServiceInfo serviceInfo : mServices) { if (packageName.equals(serviceInfo.component.getPackageName())) { return true; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 6044561c5967..7b32d20d76dc 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -9391,8 +9391,7 @@ public class PackageManagerService extends IPackageManager.Stub { } // A package name must be unique; don't allow duplicates - if (mPackages.containsKey(pkg.packageName) - || mSharedLibraries.containsKey(pkg.packageName)) { + if (mPackages.containsKey(pkg.packageName)) { throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE, "Application package " + pkg.packageName + " already installed. Skipping duplicate."); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index ae709feb4bd3..56d679ef4fa4 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1529,10 +1529,11 @@ public class ShortcutService extends IShortcutService.Stub { if (UserHandle.getUserId(callingUid) != userId) { throw new SecurityException("Invalid user-ID"); } - if (injectGetPackageUid(packageName, userId) == callingUid) { - return; // Caller is valid. + if (injectGetPackageUid(packageName, userId) != callingUid) { + throw new SecurityException("Calling package name mismatch"); } - throw new SecurityException("Calling package name mismatch"); + Preconditions.checkState(!isEphemeralApp(packageName, userId), + "Ephemeral apps can't use ShortcutManager"); } // Overridden in unit tests to execute r synchronously. @@ -3073,6 +3074,10 @@ public class ShortcutService extends IShortcutService.Stub { return (ai != null) && (ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0; } + private static boolean isEphemeralApp(@Nullable ApplicationInfo ai) { + return (ai != null) && ai.isEphemeralApp(); + } + private static boolean isInstalled(@Nullable PackageInfo pi) { return (pi != null) && isInstalled(pi.applicationInfo); } @@ -3097,6 +3102,10 @@ public class ShortcutService extends IShortcutService.Stub { return getApplicationInfo(packageName, userId) != null; } + boolean isEphemeralApp(String packageName, int userId) { + return isEphemeralApp(getApplicationInfo(packageName, userId)); + } + @Nullable XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { return activityInfo.loadXmlMetaData(mContext.getPackageManager(), key); diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index 02b46ecfc258..4fd51b2274df 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -35,6 +35,7 @@ import android.provider.Settings.Global; import android.provider.Settings; import android.util.AndroidRuntimeException; import android.util.Log; +import android.webkit.UserPackage; import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewZygote; @@ -271,6 +272,12 @@ public class SystemImpl implements SystemInterface { } @Override + public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, + WebViewProviderInfo configInfo) { + return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS); + } + + @Override public int getMultiProcessSetting(Context context) { return Settings.Global.getInt(context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java index fd137eb245d4..b06f82971247 100644 --- a/services/core/java/com/android/server/webkit/SystemInterface.java +++ b/services/core/java/com/android/server/webkit/SystemInterface.java @@ -20,8 +20,11 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.database.ContentObserver; +import android.webkit.UserPackage; import android.webkit.WebViewProviderInfo; +import java.util.List; + /** * System interface for the WebViewUpdateService. * This interface provides a way to test the WebView preparation mechanism - during normal use this @@ -49,6 +52,14 @@ public interface SystemInterface { public boolean systemIsDebuggable(); public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) throws NameNotFoundException; + /** + * Get the PackageInfos of all users for the package represented by {@param configInfo}. + * @return an array of UserPackages for a certain package, each UserPackage being belonging to a + * certain user. The returned array can contain null PackageInfos if the given package + * is uninstalled for some user. + */ + public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, + WebViewProviderInfo configInfo); public int getMultiProcessSetting(Context context); public void setMultiProcessSetting(Context context, int value); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 311570e893cf..4a105e18e9ba 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -94,6 +94,9 @@ public class WebViewUpdateService extends SystemService { case Intent.ACTION_USER_ADDED: mImpl.handleNewUser(userId); break; + case Intent.ACTION_USER_REMOVED: + mImpl.handleUserRemoved(userId); + break; } } }; @@ -112,6 +115,7 @@ public class WebViewUpdateService extends SystemService { IntentFilter userAddedFilter = new IntentFilter(); userAddedFilter.addAction(Intent.ACTION_USER_ADDED); + userAddedFilter.addAction(Intent.ACTION_USER_REMOVED); getContext().registerReceiverAsUser(mWebViewUpdatedReceiver, UserHandle.ALL, userAddedFilter, null /* broadcast permission */, null /* handler */); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index edfb11c6634b..f016830a2673 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -24,6 +24,7 @@ import android.os.Handler; import android.os.UserHandle; import android.util.Base64; import android.util.Slog; +import android.webkit.UserPackage; import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; @@ -100,32 +101,41 @@ public class WebViewUpdateServiceImpl { private boolean existsValidNonFallbackProvider(WebViewProviderInfo[] providers) { for (WebViewProviderInfo provider : providers) { if (provider.availableByDefault && !provider.isFallback) { - try { - PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(provider); - if (isInstalledPackage(packageInfo) && isEnabledPackage(packageInfo) - && mWebViewUpdater.isValidProvider(provider, packageInfo)) { - return true; - } - } catch (NameNotFoundException e) { - // A non-existent provider is neither valid nor enabled + // userPackages can contain null objects. + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + if (isInstalledAndEnabledForAllUsers(userPackages) && + // Checking validity of the package for the system user (rather than all + // users) since package validity depends not on the user but on the package + // itself. + mWebViewUpdater.isValidProvider(provider, + userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo())) { + return true; } } } return false; } - /** - * Called when a new user has been added to update the state of its fallback package. - */ void handleNewUser(int userId) { - if (!mSystemInterface.isFallbackLogicEnabled()) return; + handleUserChange(); + } - WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages(); - WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); - if (fallbackProvider == null) return; + void handleUserRemoved(int userId) { + handleUserChange(); + } - mSystemInterface.enablePackageForUser(fallbackProvider.packageName, - !existsValidNonFallbackProvider(webviewProviders), userId); + /** + * Called when a user was added or removed to ensure fallback logic and WebView preparation are + * triggered. This has to be done since the WebView package we use depends on the enabled-state + * of packages for all users (so adding or removing a user might cause us to change package). + */ + private void handleUserChange() { + if (mSystemInterface.isFallbackLogicEnabled()) { + updateFallbackState(mSystemInterface.getWebViewPackages()); + } + // Potentially trigger package-changing logic. + mWebViewUpdater.updateCurrentWebViewPackage(null); } void notifyRelroCreationCompleted() { @@ -141,7 +151,7 @@ public class WebViewUpdateServiceImpl { } WebViewProviderInfo[] getValidWebViewPackages() { - return mWebViewUpdater.getValidAndInstalledWebViewPackages(); + return mWebViewUpdater.getValidWebViewPackages(); } WebViewProviderInfo[] getWebViewPackages() { @@ -160,7 +170,7 @@ public class WebViewUpdateServiceImpl { if (!mSystemInterface.isFallbackLogicEnabled()) return; WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages(); - updateFallbackState(webviewProviders, true); + updateFallbackState(webviewProviders); } /** @@ -185,35 +195,23 @@ public class WebViewUpdateServiceImpl { } } if (!changedPackageAvailableByDefault) return; - updateFallbackState(webviewProviders, false); + updateFallbackState(webviewProviders); } - private void updateFallbackState(WebViewProviderInfo[] webviewProviders, boolean isBoot) { + private void updateFallbackState(WebViewProviderInfo[] webviewProviders) { // If there exists a valid and enabled non-fallback package - disable the fallback // package, otherwise, enable it. WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); if (fallbackProvider == null) return; boolean existsValidNonFallbackProvider = existsValidNonFallbackProvider(webviewProviders); - boolean isFallbackEnabled = false; - try { - isFallbackEnabled = isEnabledPackage( - mSystemInterface.getPackageInfoForProvider(fallbackProvider)); - } catch (NameNotFoundException e) { - // No fallback package installed -> early out. - return; - } - - if (existsValidNonFallbackProvider - // During an OTA the primary user's WebView state might differ from other users', so - // ignore the state of that user during boot. - && (isFallbackEnabled || isBoot)) { + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers(mContext, fallbackProvider); + if (existsValidNonFallbackProvider && !isDisabledForAllUsers(userPackages)) { mSystemInterface.uninstallAndDisablePackageForAllUsers(mContext, fallbackProvider.packageName); } else if (!existsValidNonFallbackProvider - // During an OTA the primary user's WebView state might differ from other users', so - // ignore the state of that user during boot. - && (!isFallbackEnabled || isBoot)) { + && !isInstalledAndEnabledForAllUsers(userPackages)) { // Enable the fallback package for all users. mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, true); @@ -376,38 +374,8 @@ public class WebViewUpdateServiceImpl { * or the replacement are done). */ public String changeProviderAndSetting(String newProviderName) { - PackageInfo oldPackage = null; - PackageInfo newPackage = null; - boolean providerChanged = false; - synchronized(mLock) { - oldPackage = mCurrentWebViewPackage; - mSystemInterface.updateUserSetting(mContext, newProviderName); - - try { - newPackage = findPreferredWebViewPackage(); - providerChanged = (oldPackage == null) - || !newPackage.packageName.equals(oldPackage.packageName); - } catch (WebViewPackageMissingException e) { - mCurrentWebViewPackage = null; - Slog.e(TAG, "Tried to change WebView provider but failed to fetch WebView " + - "package " + e); - // If we don't perform the user change but don't have an installed WebView - // package, we will have changed the setting and it will be used when a package - // is available. - return ""; - } - // Perform the provider change if we chose a new provider - if (providerChanged) { - onWebViewProviderChanged(newPackage); - } - } - // Kill apps using the old provider only if we changed provider - if (providerChanged && oldPackage != null) { - mSystemInterface.killPackageDependents(oldPackage.packageName); - } - // Return the new provider, this is not necessarily the one we were asked to switch to - // But the persistent setting will now be pointing to the provider we were asked to - // switch to anyway + PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName); + if (newPackage == null) return ""; return newPackage.packageName; } @@ -437,15 +405,14 @@ public class WebViewUpdateServiceImpl { } } - private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos(boolean onlyInstalled) { + private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos() { WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages(); List<ProviderAndPackageInfo> providers = new ArrayList<>(); for(int n = 0; n < allProviders.length; n++) { try { PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(allProviders[n]); - if ((!onlyInstalled || isInstalledPackage(packageInfo)) - && isValidProvider(allProviders[n], packageInfo)) { + if (isValidProvider(allProviders[n], packageInfo)) { providers.add(new ProviderAndPackageInfo(allProviders[n], packageInfo)); } } catch (NameNotFoundException e) { @@ -458,9 +425,8 @@ public class WebViewUpdateServiceImpl { /** * Fetch only the currently valid WebView packages. **/ - public WebViewProviderInfo[] getValidAndInstalledWebViewPackages() { - ProviderAndPackageInfo[] providersAndPackageInfos = - getValidWebViewPackagesAndInfos(true /* only fetch installed packages */); + public WebViewProviderInfo[] getValidWebViewPackages() { + ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos(); WebViewProviderInfo[] providers = new WebViewProviderInfo[providersAndPackageInfos.length]; for(int n = 0; n < providersAndPackageInfos.length; n++) { @@ -487,39 +453,49 @@ public class WebViewUpdateServiceImpl { * */ private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { - ProviderAndPackageInfo[] providers = - getValidWebViewPackagesAndInfos(false /* onlyInstalled */); + ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext); - // If the user has chosen provider, use that + // If the user has chosen provider, use that (if it's installed and enabled for all + // users). for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.packageName.equals(userChosenProvider) - && isInstalledPackage(providerAndPackage.packageInfo) - && isEnabledPackage(providerAndPackage.packageInfo)) { - return providerAndPackage.packageInfo; + if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { + // userPackages can contain null objects. + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + providerAndPackage.provider); + if (isInstalledAndEnabledForAllUsers(userPackages)) { + return providerAndPackage.packageInfo; + } } } // User did not choose, or the choice failed; use the most stable provider that is - // installed and enabled for the device owner, and available by default (not through + // installed and enabled for all users, and available by default (not through // user choice). for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.availableByDefault - && isInstalledPackage(providerAndPackage.packageInfo) - && isEnabledPackage(providerAndPackage.packageInfo)) { - return providerAndPackage.packageInfo; + if (providerAndPackage.provider.availableByDefault) { + // userPackages can contain null objects. + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + providerAndPackage.provider); + if (isInstalledAndEnabledForAllUsers(userPackages)) { + return providerAndPackage.packageInfo; + } } } // Could not find any installed and enabled package either, use the most stable and // default-available provider. + // TODO(gsennton) remove this when we have a functional WebView stub. for (ProviderAndPackageInfo providerAndPackage : providers) { if (providerAndPackage.provider.availableByDefault) { return providerAndPackage.packageInfo; } } + // This should never happen during normal operation (only with modified system images). mAnyWebViewInstalled = false; throw new WebViewPackageMissingException("Could not find a loadable WebView package"); } @@ -702,6 +678,48 @@ public class WebViewUpdateServiceImpl { mAnyWebViewInstalled)); } } + + /** + * Update the current WebView package. + * @param newProviderName the package to switch to, null if no package has been explicitly + * chosen. + */ + public PackageInfo updateCurrentWebViewPackage(String newProviderName) { + PackageInfo oldPackage = null; + PackageInfo newPackage = null; + boolean providerChanged = false; + synchronized(mLock) { + oldPackage = mCurrentWebViewPackage; + + if (newProviderName != null) { + mSystemInterface.updateUserSetting(mContext, newProviderName); + } + + try { + newPackage = findPreferredWebViewPackage(); + providerChanged = (oldPackage == null) + || !newPackage.packageName.equals(oldPackage.packageName); + } catch (WebViewPackageMissingException e) { + // If updated the Setting but don't have an installed WebView package, the + // Setting will be used when a package is available. + mCurrentWebViewPackage = null; + Slog.e(TAG, "Couldn't find WebView package to use " + e); + return null; + } + // Perform the provider change if we chose a new provider + if (providerChanged) { + onWebViewProviderChanged(newPackage); + } + } + // Kill apps using the old provider only if we changed provider + if (providerChanged && oldPackage != null) { + mSystemInterface.killPackageDependents(oldPackage.packageName); + } + // Return the new provider, this is not necessarily the one we were asked to switch to, + // but the persistent setting will now be pointing to the provider we were asked to + // switch to anyway. + return newPackage; + } } private static boolean providerHasValidSignature(WebViewProviderInfo provider, @@ -730,20 +748,27 @@ public class WebViewUpdateServiceImpl { } /** - * Returns whether the given package is enabled. - * This state can be changed by the user from Settings->Apps + * Return true iff {@param packageInfos} point to only installed and enabled packages. + * The given packages {@param packageInfos} should all be pointing to the same package, but each + * PackageInfo representing a different user's package. */ - private static boolean isEnabledPackage(PackageInfo packageInfo) { - return packageInfo.applicationInfo.enabled; + private static boolean isInstalledAndEnabledForAllUsers( + List<UserPackage> userPackages) { + for (UserPackage userPackage : userPackages) { + if (!userPackage.isInstalledPackage() || !userPackage.isEnabledPackage()) { + return false; + } + } + return true; } - /** - * Return true if the package is installed and not hidden - */ - private static boolean isInstalledPackage(PackageInfo packageInfo) { - return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) - && ((packageInfo.applicationInfo.privateFlags - & ApplicationInfo.PRIVATE_FLAG_HIDDEN) == 0)); + private static boolean isDisabledForAllUsers(List<UserPackage> userPackages) { + for (UserPackage userPackage : userPackages) { + if (userPackage.getPackageInfo() != null && userPackage.isEnabledPackage()) { + return false; + } + } + return true; } /** diff --git a/services/core/jni/com_android_server_lights_LightsService.cpp b/services/core/jni/com_android_server_lights_LightsService.cpp index 2fd0603c8f89..74af639cd9af 100644 --- a/services/core/jni/com_android_server_lights_LightsService.cpp +++ b/services/core/jni/com_android_server_lights_LightsService.cpp @@ -81,7 +81,7 @@ static void setLight_native( // TODO(b/31632518) if (gLight == nullptr) { - gLight = ILight::getService("light"); + gLight = ILight::getService(); } if (gLight == nullptr) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index bedc6e1d9057..c3ef23b92fb4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -280,7 +280,7 @@ public final class SystemServer { Slog.i(TAG, "Entered the Android system server!"); int uptimeMillis = (int) SystemClock.elapsedRealtime(); EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_SYSTEM_RUN, uptimeMillis); - if (!mRuntimeRestart && !mFirstBoot) { + if (!mRuntimeRestart) { MetricsLogger.histogram(null, "boot_system_server_init", uptimeMillis); } @@ -349,7 +349,6 @@ public final class SystemServer { // Create the system service manager. mSystemServiceManager = new SystemServiceManager(mSystemContext); mSystemServiceManager.setRuntimeRestarted(mRuntimeRestart); - mSystemServiceManager.setFirstBoot(mFirstBoot); LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); // Prepare the thread pool for init tasks that can be parallelized SystemServerInitThreadPool.get(); @@ -376,7 +375,7 @@ public final class SystemServer { if (StrictMode.conditionallyEnableDebugLogging()) { Slog.i(TAG, "Enabled StrictMode for system server main thread."); } - if (!mRuntimeRestart && !mFirstBoot) { + if (!mRuntimeRestart && !isFirstBootOrUpgrade()) { int uptimeMillis = (int) SystemClock.elapsedRealtime(); MetricsLogger.histogram(null, "boot_system_server_ready", uptimeMillis); final int MAX_UPTIME_MILLIS = 60 * 1000; @@ -391,6 +390,10 @@ public final class SystemServer { throw new RuntimeException("Main thread loop unexpectedly exited"); } + private boolean isFirstBootOrUpgrade() { + return mPackageManagerService.isFirstBoot() || mPackageManagerService.isUpgrade(); + } + private void reportWtf(String msg, Throwable e) { Slog.w(TAG, "***********************************************"); Slog.wtf(TAG, "BOOT FAILURE " + msg, e); @@ -535,7 +538,7 @@ public final class SystemServer { mFirstBoot = mPackageManagerService.isFirstBoot(); mPackageManager = mSystemContext.getPackageManager(); traceEnd(); - if (!mRuntimeRestart && !mFirstBoot) { + if (!mRuntimeRestart && !isFirstBootOrUpgrade()) { MetricsLogger.histogram(null, "boot_package_manager_init_ready", (int) SystemClock.elapsedRealtime()); } @@ -936,6 +939,12 @@ public final class SystemServer { traceEnd(); } + if (!disableNonCoreServices) { + traceBeginAndSlog("StartFontServiceManager"); + mSystemServiceManager.startService(FontManagerService.Lifecycle.class); + traceEnd(); + } + if (!disableNonCoreServices && !disableTextServices) { traceBeginAndSlog("StartTextServicesManager"); mSystemServiceManager.startService(TextServicesManagerService.Lifecycle.class); diff --git a/services/tests/notification/AndroidManifest.xml b/services/tests/notification/AndroidManifest.xml index 1ed8ed0c5194..92f155f53420 100644 --- a/services/tests/notification/AndroidManifest.xml +++ b/services/tests/notification/AndroidManifest.xml @@ -22,6 +22,7 @@ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.MANAGE_USERS" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" /> <application> <uses-library android:name="android.test.runner" /> diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java index 40938fd88ef1..9b74fcc864e9 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,7 +24,9 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,18 +35,25 @@ import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.MessageQueue; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.support.test.annotation.UiThreadTest; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -58,30 +67,76 @@ import com.android.server.lights.LightsManager; @RunWith(AndroidJUnit4.class) public class NotificationManagerServiceTest { private final String pkg = "com.android.server.notification"; - private final int uid = 0; + private final int uid = Binder.getCallingUid(); private NotificationManagerService mNotificationManagerService; private INotificationManager mBinderService; private IPackageManager mPackageManager = mock(IPackageManager.class); + private Context mContext; + private HandlerThread mThread; @Before @UiThreadTest public void setUp() throws Exception { - final Context context = InstrumentationRegistry.getTargetContext(); - mNotificationManagerService = new NotificationManagerService(context); + mContext = InstrumentationRegistry.getTargetContext(); + mNotificationManagerService = new NotificationManagerService(mContext); // MockPackageManager - default returns ApplicationInfo with matching calling UID final ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.uid = Binder.getCallingUid(); + applicationInfo.uid = uid; when(mPackageManager.getApplicationInfo(any(), anyInt(), anyInt())) .thenReturn(applicationInfo); + final PackageManager mockPackageManagerClient = mock(PackageManager.class); + when(mockPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); final LightsManager mockLightsManager = mock(LightsManager.class); when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class)); - mNotificationManagerService.init(mPackageManager, mockLightsManager); + // Use a separate thread for service looper. + mThread = new HandlerThread("TestThread"); + mThread.start(); + // Mock NotificationListeners to bypass security checks. + final NotificationManagerService.NotificationListeners mockNotificationListeners = + mock(NotificationManagerService.NotificationListeners.class); + when(mockNotificationListeners.checkServiceTokenLocked(any())).thenReturn( + mockNotificationListeners.new ManagedServiceInfo(null, + new ComponentName(pkg, "test_class"), uid, true, null, 0)); + + mNotificationManagerService.init(mThread.getLooper(), mPackageManager, + mockPackageManagerClient, mockLightsManager, mockNotificationListeners); // Tests call directly into the Binder. mBinderService = mNotificationManagerService.getBinderService(); } + public void waitForIdle() throws Exception { + MessageQueue queue = mThread.getLooper().getQueue(); + CountDownLatch latch = new CountDownLatch(1); + queue.addIdleHandler(new MessageQueue.IdleHandler() { + @Override public boolean queueIdle() { + latch.countDown(); + return false; + } + }); + latch.await(); + if (!queue.isIdle()) { + waitForIdle(); + } + } + + private NotificationRecord generateNotificationRecord(NotificationChannel channel) { + if (channel == null) { + channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT); + } + Notification n = new Notification.Builder(mContext) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mContext.getPackageName(), + mContext.getPackageName(), channel, 1, "tag", uid, 0, + n, new UserHandle(uid), null, 0); + return new NotificationRecord(mContext, sbn); + } + @Test @UiThreadTest public void testCreateNotificationChannels_SingleChannel() throws Exception { @@ -203,15 +258,120 @@ public class NotificationManagerServiceTest { verify(usageStats, times(1)).registerBlocked(eq(r)); } - private NotificationRecord generateNotificationRecord(NotificationChannel channel) { - final Context context = InstrumentationRegistry.getTargetContext(); - Notification n = new Notification.Builder(context) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setPriority(Notification.PRIORITY_HIGH) - .build(); - StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, channel, 1, "tag", uid, uid, - n, UserHandle.SYSTEM, null, uid); - return new NotificationRecord(context, sbn); + @Test + @UiThreadTest + public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception { + mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0, + generateNotificationRecord(null).getNotification(), new int[1], 0); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(mContext.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception { + mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0, + generateNotificationRecord(null).getNotification(), new int[1], 0); + mBinderService.cancelNotificationWithTag(mContext.getPackageName(), "tag", 0, 0); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(mContext.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelNotificationsFromListener(null, null); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotificationsImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_IgnoreForegroundService() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_IgnoreOtherPackages() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications("other_pkg_name", sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_NullPkgRemovesAll() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(null, sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_NullPkgIgnoresUserAllNotifications() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], UserHandle.USER_ALL); + // Null pkg is how we signal a user switch. + mBinderService.cancelAllNotifications(null, sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testSnoozeNotificationImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.snoozeNotificationFromListener(null, sbn.getKey()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); } } diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java index ee49a00c7f40..a600e69bd345 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java @@ -76,6 +76,7 @@ import org.mockito.MockitoAnnotations; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -94,6 +95,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { @Mock private DevicePolicyManager mMockDevicePolicyManager; @Mock private IAccountManagerResponse mMockAccountManagerResponse; @Mock private IBinder mMockBinder; + @Mock private INotificationManager mMockNotificationManager; @Captor private ArgumentCaptor<Intent> mIntentCaptor; @Captor private ArgumentCaptor<Bundle> mBundleCaptor; @@ -129,7 +131,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { Context realTestContext = getContext(); MyMockContext mockContext = new MyMockContext(realTestContext, mMockContext); setContext(mockContext); - mTestInjector = new TestInjector(realTestContext, mockContext); + mTestInjector = new TestInjector(realTestContext, mockContext, mMockNotificationManager); mAms = new AccountManagerService(mTestInjector); } @@ -500,7 +502,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest - public void testStartAddAccountSessionUserSuccessWithoutPasswordForwarding() throws Exception { + public void testStartAddAccountSessionSuccessWithoutPasswordForwarding() throws Exception { unlockSystemUser(); when(mMockContext.checkCallingOrSelfPermission(anyString())).thenReturn( PackageManager.PERMISSION_DENIED); @@ -531,7 +533,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest - public void testStartAddAccountSessionUserSuccessWithPasswordForwarding() throws Exception { + public void testStartAddAccountSessionSuccessWithPasswordForwarding() throws Exception { unlockSystemUser(); when(mMockContext.checkCallingOrSelfPermission(anyString())).thenReturn( PackageManager.PERMISSION_GRANTED); @@ -564,7 +566,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest - public void testStartAddAccountSessionUserReturnWithInvalidIntent() throws Exception { + public void testStartAddAccountSessionReturnWithInvalidIntent() throws Exception { unlockSystemUser(); ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.activityInfo = new ActivityInfo(); @@ -593,7 +595,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest - public void testStartAddAccountSessionUserReturnWithValidIntent() throws Exception { + public void testStartAddAccountSessionReturnWithValidIntent() throws Exception { unlockSystemUser(); ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.activityInfo = new ActivityInfo(); @@ -626,7 +628,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest - public void testStartAddAccountSessionUserError() throws Exception { + public void testStartAddAccountSessionError() throws Exception { unlockSystemUser(); Bundle options = createOptionsWithAccountName( AccountManagerServiceTestFixtures.ACCOUNT_NAME_ERROR); @@ -650,14 +652,629 @@ public class AccountManagerServiceTest extends AndroidTestCase { verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); } + @SmallTest + public void testStartUpdateCredentialsSessionWithNullResponse() throws Exception { + unlockSystemUser(); + try { + mAms.startUpdateCredentialsSession( + null, // response + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + "authTokenType", + true, // expectActivityLaunch + null); // optionsIn + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testStartUpdateCredentialsSessionWithNullAccount() throws Exception { + unlockSystemUser(); + try { + mAms.startUpdateCredentialsSession( + mMockAccountManagerResponse, // response + null, + "authTokenType", + true, // expectActivityLaunch + null); // optionsIn + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testStartUpdateCredentialsSessionSuccessWithoutPasswordForwarding() + throws Exception { + unlockSystemUser(); + when(mMockContext.checkCallingOrSelfPermission(anyString())).thenReturn( + PackageManager.PERMISSION_DENIED); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + Bundle options = createOptionsWithAccountName( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS); + mAms.startUpdateCredentialsSession( + response, // response + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + "authTokenType", + false, // expectActivityLaunch + options); // optionsIn + waitForLatch(latch); + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); + assertNotNull(sessionBundle); + // Assert that session bundle is encrypted and hence data not visible. + assertNull(sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1)); + // Assert password is not returned + assertNull(result.getString(AccountManager.KEY_PASSWORD)); + assertNull(result.getString(AccountManager.KEY_AUTHTOKEN, null)); + assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN, + result.getString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN)); + } + + @SmallTest + public void testStartUpdateCredentialsSessionSuccessWithPasswordForwarding() throws Exception { + unlockSystemUser(); + when(mMockContext.checkCallingOrSelfPermission(anyString())).thenReturn( + PackageManager.PERMISSION_GRANTED); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + Bundle options = createOptionsWithAccountName( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS); + mAms.startUpdateCredentialsSession( + response, // response + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + "authTokenType", + false, // expectActivityLaunch + options); // optionsIn + + waitForLatch(latch); + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); + assertNotNull(sessionBundle); + // Assert that session bundle is encrypted and hence data not visible. + assertNull(sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1)); + // Assert password is returned + assertEquals(result.getString(AccountManager.KEY_PASSWORD), + AccountManagerServiceTestFixtures.ACCOUNT_PASSWORD); + assertNull(result.getString(AccountManager.KEY_AUTHTOKEN)); + assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN, + result.getString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN)); + } + + @SmallTest + public void testStartUpdateCredentialsSessionReturnWithInvalidIntent() throws Exception { + unlockSystemUser(); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + when(mMockPackageManager.resolveActivityAsUser( + any(Intent.class), anyInt(), anyInt())).thenReturn(resolveInfo); + when(mMockPackageManager.checkSignatures( + anyInt(), anyInt())).thenReturn(PackageManager.SIGNATURE_NO_MATCH); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + Bundle options = createOptionsWithAccountName( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_INTERVENE); + + mAms.startUpdateCredentialsSession( + response, // response + AccountManagerServiceTestFixtures.ACCOUNT_INTERVENE, + "authTokenType", + true, // expectActivityLaunch + options); // optionsIn + + waitForLatch(latch); + verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_REMOTE_EXCEPTION), anyString()); + } + + @SmallTest + public void testStartUpdateCredentialsSessionReturnWithValidIntent() throws Exception { + unlockSystemUser(); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + when(mMockPackageManager.resolveActivityAsUser( + any(Intent.class), anyInt(), anyInt())).thenReturn(resolveInfo); + when(mMockPackageManager.checkSignatures( + anyInt(), anyInt())).thenReturn(PackageManager.SIGNATURE_MATCH); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + Bundle options = createOptionsWithAccountName( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_INTERVENE); + + mAms.startUpdateCredentialsSession( + response, // response + AccountManagerServiceTestFixtures.ACCOUNT_INTERVENE, + "authTokenType", + true, // expectActivityLaunch + options); // optionsIn + + waitForLatch(latch); + + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + assertNotNull(intent); + assertNotNull(intent.getParcelableExtra(AccountManagerServiceTestFixtures.KEY_RESULT)); + assertNotNull(intent.getParcelableExtra(AccountManagerServiceTestFixtures.KEY_CALLBACK)); + } + + @SmallTest + public void testStartUpdateCredentialsSessionError() throws Exception { + unlockSystemUser(); + Bundle options = createOptionsWithAccountName( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_ERROR); + options.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_INVALID_RESPONSE); + options.putString(AccountManager.KEY_ERROR_MESSAGE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.startUpdateCredentialsSession( + response, // response + AccountManagerServiceTestFixtures.ACCOUNT_ERROR, + "authTokenType", + true, // expectActivityLaunch + options); // optionsIn + + waitForLatch(latch); + verify(mMockAccountManagerResponse).onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); + verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); + } + + @SmallTest + public void testFinishSessionAsUserWithNullResponse() throws Exception { + unlockSystemUser(); + try { + mAms.finishSessionAsUser( + null, // response + createEncryptedSessionBundle( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testFinishSessionAsUserWithNullSessionBundle() throws Exception { + unlockSystemUser(); + try { + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + null, // sessionBundle + false, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testFinishSessionAsUserUserCannotModifyAccountNoDPM() throws Exception { + unlockSystemUser(); + Bundle bundle = new Bundle(); + bundle.putBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, true); + when(mMockUserManager.getUserRestrictions(any(UserHandle.class))).thenReturn(bundle); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_USER_RESTRICTED), anyString()); + verify(mMockContext).startActivityAsUser(mIntentCaptor.capture(), eq(UserHandle.of(2))); + + // verify the intent for default CantAddAccountActivity is sent. + Intent intent = mIntentCaptor.getValue(); + assertEquals(intent.getComponent().getClassName(), CantAddAccountActivity.class.getName()); + assertEquals(intent.getIntExtra(CantAddAccountActivity.EXTRA_ERROR_CODE, 0), + AccountManager.ERROR_CODE_USER_RESTRICTED); + } + + @SmallTest + public void testFinishSessionAsUserUserCannotModifyAccountWithDPM() throws Exception { + unlockSystemUser(); + Bundle bundle = new Bundle(); + bundle.putBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, true); + when(mMockUserManager.getUserRestrictions(any(UserHandle.class))).thenReturn(bundle); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService( + DevicePolicyManagerInternal.class, mMockDevicePolicyManagerInternal); + when(mMockDevicePolicyManagerInternal.createUserRestrictionSupportIntent( + anyInt(), anyString())).thenReturn(new Intent()); + when(mMockDevicePolicyManagerInternal.createShowAdminSupportIntent( + anyInt(), anyBoolean())).thenReturn(new Intent()); + + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_USER_RESTRICTED), anyString()); + verify(mMockContext).startActivityAsUser(any(Intent.class), eq(UserHandle.of(2))); + verify(mMockDevicePolicyManagerInternal).createUserRestrictionSupportIntent( + anyInt(), anyString()); + } + + @SmallTest + public void testFinishSessionAsUserWithBadSessionBundle() throws Exception { + unlockSystemUser(); + + Bundle badSessionBundle = new Bundle(); + badSessionBundle.putString("any", "any"); + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + badSessionBundle, // sessionBundle + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_BAD_REQUEST), anyString()); + } + + @SmallTest + public void testFinishSessionAsUserWithBadAccountType() throws Exception { + unlockSystemUser(); + + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + createEncryptedSessionBundleWithNoAccountType( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_BAD_ARGUMENTS), anyString()); + } + + @SmallTest + public void testFinishSessionAsUserUserCannotModifyAccountForTypeNoDPM() throws Exception { + unlockSystemUser(); + when(mMockDevicePolicyManager.getAccountTypesWithManagementDisabledAsUser(anyInt())) + .thenReturn(new String[]{AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1, "BBB"}); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE), anyString()); + verify(mMockContext).startActivityAsUser(mIntentCaptor.capture(), eq(UserHandle.of(2))); + + // verify the intent for default CantAddAccountActivity is sent. + Intent intent = mIntentCaptor.getValue(); + assertEquals(intent.getComponent().getClassName(), CantAddAccountActivity.class.getName()); + assertEquals(intent.getIntExtra(CantAddAccountActivity.EXTRA_ERROR_CODE, 0), + AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE); + } + + @SmallTest + public void testFinishSessionAsUserUserCannotModifyAccountForTypeWithDPM() throws Exception { + unlockSystemUser(); + when(mMockContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn( + mMockDevicePolicyManager); + when(mMockDevicePolicyManager.getAccountTypesWithManagementDisabledAsUser(anyInt())) + .thenReturn(new String[]{AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1, "BBB"}); + + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService( + DevicePolicyManagerInternal.class, mMockDevicePolicyManagerInternal); + when(mMockDevicePolicyManagerInternal.createUserRestrictionSupportIntent( + anyInt(), anyString())).thenReturn(new Intent()); + when(mMockDevicePolicyManagerInternal.createShowAdminSupportIntent( + anyInt(), anyBoolean())).thenReturn(new Intent()); + + mAms.finishSessionAsUser( + mMockAccountManagerResponse, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + 2); // fake user id + + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE), anyString()); + verify(mMockContext).startActivityAsUser(any(Intent.class), eq(UserHandle.of(2))); + verify(mMockDevicePolicyManagerInternal).createShowAdminSupportIntent( + anyInt(), anyBoolean()); + } + + @SmallTest + public void testFinishSessionAsUserSuccess() throws Exception { + unlockSystemUser(); + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + mAms.finishSessionAsUser( + response, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS), + false, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + + waitForLatch(latch); + // Verify notification is cancelled + verify(mMockNotificationManager).cancelNotificationWithTag( + anyString(), anyString(), anyInt(), anyInt()); + + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); + assertNotNull(sessionBundle); + // Assert that session bundle is decrypted and hence data is visible. + assertEquals(AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1, + sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1)); + // Assert finishSessionAsUser added calling uid and pid into the sessionBundle + assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_UID)); + assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_PID)); + // Assert App bundle data overrides sessionBundle data + assertEquals(sessionBundle.getString( + AccountManager.KEY_ANDROID_PACKAGE_NAME), "APCT.package"); + + // Verify response data + assertNull(result.getString(AccountManager.KEY_AUTHTOKEN, null)); + assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_NAME, + result.getString(AccountManager.KEY_ACCOUNT_NAME)); + assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1, + result.getString(AccountManager.KEY_ACCOUNT_TYPE)); + } + + @SmallTest + public void testFinishSessionAsUserReturnWithInvalidIntent() throws Exception { + unlockSystemUser(); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + when(mMockPackageManager.resolveActivityAsUser( + any(Intent.class), anyInt(), anyInt())).thenReturn(resolveInfo); + when(mMockPackageManager.checkSignatures( + anyInt(), anyInt())).thenReturn(PackageManager.SIGNATURE_NO_MATCH); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.finishSessionAsUser( + response, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_INTERVENE), + true, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + + waitForLatch(latch); + verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); + verify(mMockAccountManagerResponse).onError( + eq(AccountManager.ERROR_CODE_REMOTE_EXCEPTION), anyString()); + } + + @SmallTest + public void testFinishSessionAsUserReturnWithValidIntent() throws Exception { + unlockSystemUser(); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + when(mMockPackageManager.resolveActivityAsUser( + any(Intent.class), anyInt(), anyInt())).thenReturn(resolveInfo); + when(mMockPackageManager.checkSignatures( + anyInt(), anyInt())).thenReturn(PackageManager.SIGNATURE_MATCH); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.finishSessionAsUser( + response, // response + createEncryptedSessionBundle(AccountManagerServiceTestFixtures.ACCOUNT_NAME_INTERVENE), + true, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + + waitForLatch(latch); + + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + assertNotNull(intent); + assertNotNull(intent.getParcelableExtra(AccountManagerServiceTestFixtures.KEY_RESULT)); + assertNotNull(intent.getParcelableExtra(AccountManagerServiceTestFixtures.KEY_CALLBACK)); + } + + @SmallTest + public void testFinishSessionAsUserError() throws Exception { + unlockSystemUser(); + Bundle sessionBundle = createEncryptedSessionBundleWithError( + AccountManagerServiceTestFixtures.ACCOUNT_NAME_ERROR); + + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.finishSessionAsUser( + response, // response + sessionBundle, + false, // expectActivityLaunch + createAppBundle(), // appInfo + UserHandle.USER_SYSTEM); + + waitForLatch(latch); + verify(mMockAccountManagerResponse).onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); + verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); + } + + @SmallTest + public void testIsCredentialsUpdatedSuggestedWithNullResponse() throws Exception { + unlockSystemUser(); + try { + mAms.isCredentialsUpdateSuggested( + null, // response + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN); + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testIsCredentialsUpdatedSuggestedWithNullAccount() throws Exception { + unlockSystemUser(); + try { + mAms.isCredentialsUpdateSuggested( + mMockAccountManagerResponse, + null, // account + AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN); + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testIsCredentialsUpdatedSuggestedWithEmptyStatusToken() throws Exception { + unlockSystemUser(); + try { + mAms.isCredentialsUpdateSuggested( + mMockAccountManagerResponse, + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + null); + fail("IllegalArgumentException expected. But no exception was thrown."); + } catch (IllegalArgumentException e) { + } catch(Exception e){ + fail(String.format("Expect IllegalArgumentException, but got %s.", e)); + } + } + + @SmallTest + public void testIsCredentialsUpdatedSuggestedError() throws Exception { + unlockSystemUser(); + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.isCredentialsUpdateSuggested( + response, + AccountManagerServiceTestFixtures.ACCOUNT_ERROR, + AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN); + + waitForLatch(latch); + verify(mMockAccountManagerResponse).onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); + verify(mMockAccountManagerResponse, never()).onResult(any(Bundle.class)); + } + + @SmallTest + public void testIsCredentialsUpdatedSuggestedSuccess() throws Exception { + unlockSystemUser(); + final CountDownLatch latch = new CountDownLatch(1); + Response response = new Response(latch, mMockAccountManagerResponse); + + mAms.isCredentialsUpdateSuggested( + response, + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN); + + waitForLatch(latch); + verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); + Bundle result = mBundleCaptor.getValue(); + boolean needUpdate = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + assertTrue(needUpdate); + } + private void waitForLatch(CountDownLatch latch) { try { latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - fail("should not throw an InterruptedException"); + throw new IllegalStateException("Should not throw an InterruptedException", e); } } + private Bundle encryptBundleWithCryptoHelper(Bundle sessionBundle) { + Bundle encryptedBundle = null; + try { + CryptoHelper cryptoHelper = CryptoHelper.getInstance(); + encryptedBundle = cryptoHelper.encryptBundle(sessionBundle); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Failed to encrypt session bundle.", e); + } + return encryptedBundle; + } + + private Bundle createEncryptedSessionBundle(final String accountName) { + Bundle sessionBundle = new Bundle(); + sessionBundle.putString(AccountManagerServiceTestFixtures.KEY_ACCOUNT_NAME, accountName); + sessionBundle.putString( + AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1, + AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1); + sessionBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, + AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1); + sessionBundle.putString(AccountManager.KEY_ANDROID_PACKAGE_NAME, "APCT.session.package"); + return encryptBundleWithCryptoHelper(sessionBundle); + } + + private Bundle createEncryptedSessionBundleWithError(final String accountName) { + Bundle sessionBundle = new Bundle(); + sessionBundle.putString(AccountManagerServiceTestFixtures.KEY_ACCOUNT_NAME, accountName); + sessionBundle.putString( + AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1, + AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1); + sessionBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, + AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1); + sessionBundle.putString(AccountManager.KEY_ANDROID_PACKAGE_NAME, "APCT.session.package"); + sessionBundle.putInt( + AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_INVALID_RESPONSE); + sessionBundle.putString(AccountManager.KEY_ERROR_MESSAGE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); + return encryptBundleWithCryptoHelper(sessionBundle); + } + + private Bundle createEncryptedSessionBundleWithNoAccountType(final String accountName) { + Bundle sessionBundle = new Bundle(); + sessionBundle.putString(AccountManagerServiceTestFixtures.KEY_ACCOUNT_NAME, accountName); + sessionBundle.putString( + AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1, + AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1); + sessionBundle.putString(AccountManager.KEY_ANDROID_PACKAGE_NAME, "APCT.session.package"); + return encryptBundleWithCryptoHelper(sessionBundle); + } + + private Bundle createAppBundle() { + Bundle appBundle = new Bundle(); + appBundle.putString(AccountManager.KEY_ANDROID_PACKAGE_NAME, "APCT.package"); + return appBundle; + } + private Bundle createOptionsWithAccountName(final String accountName) { Bundle sessionBundle = new Bundle(); sessionBundle.putString( @@ -784,9 +1401,13 @@ public class AccountManagerServiceTest extends AndroidTestCase { static class TestInjector extends AccountManagerService.Injector { private Context mRealContext; - TestInjector(Context realContext, Context mockContext) { + private INotificationManager mMockNotificationManager; + TestInjector(Context realContext, + Context mockContext, + INotificationManager mockNotificationManager) { super(mockContext); mRealContext = realContext; + mMockNotificationManager = mockNotificationManager; } @Override @@ -820,7 +1441,7 @@ public class AccountManagerServiceTest extends AndroidTestCase { @Override INotificationManager getNotificationManager() { - return mock(INotificationManager.class); + return mMockNotificationManager; } } diff --git a/services/tests/servicestests/src/com/android/server/accounts/TestAccountType1Authenticator.java b/services/tests/servicestests/src/com/android/server/accounts/TestAccountType1Authenticator.java index 0db11e0cecd3..8ec61763cdb8 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/TestAccountType1Authenticator.java +++ b/services/tests/servicestests/src/com/android/server/accounts/TestAccountType1Authenticator.java @@ -197,7 +197,8 @@ public class TestAccountType1Authenticator extends AbstractAccountAuthenticator result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle); result.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN, AccountManagerServiceTestFixtures.ACCOUNT_STATUS_TOKEN); - result.putString(AccountManager.KEY_PASSWORD, "doesn't matter"); + result.putString(AccountManager.KEY_PASSWORD, + AccountManagerServiceTestFixtures.ACCOUNT_PASSWORD); result.putString(AccountManager.KEY_AUTHTOKEN, Integer.toString(mTokenCounter.incrementAndGet())); } else if (accountName.equals( @@ -243,6 +244,8 @@ public class TestAccountType1Authenticator extends AbstractAccountAuthenticator Bundle result = new Bundle(); if (accountName.equals(AccountManagerServiceTestFixtures.ACCOUNT_NAME_SUCCESS)) { + // add sessionBundle into result for verification purpose + result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle); // fill bundle with a success result. result.putString(AccountManager.KEY_ACCOUNT_NAME, AccountManagerServiceTestFixtures.ACCOUNT_NAME); @@ -288,7 +291,9 @@ public class TestAccountType1Authenticator extends AbstractAccountAuthenticator } else { // fill with error fillResultWithError( - result, AccountManager.ERROR_CODE_INVALID_RESPONSE, "Default Error Message"); + result, + AccountManager.ERROR_CODE_INVALID_RESPONSE, + AccountManagerServiceTestFixtures.ERROR_MESSAGE); } response.onResult(result); diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 167b33ac31a7..9835c88c98c2 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -566,6 +566,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected Map<String, PackageInfo> mInjectedPackages; protected Set<PackageWithUser> mUninstalledPackages; + protected Set<PackageWithUser> mEphemeralPackages; protected Set<String> mSystemPackages; protected PackageManager mMockPackageManager; @@ -731,6 +732,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { mUninstalledPackages = new HashSet<>(); mSystemPackages = new HashSet<>(); + mEphemeralPackages = new HashSet<>(); mInjectedFilePathRoot = new File(getTestContext().getCacheDir(), "test-files"); @@ -1034,6 +1036,9 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { if (mUninstalledPackages.contains(PackageWithUser.of(userId, packageName))) { ret.applicationInfo.flags &= ~ApplicationInfo.FLAG_INSTALLED; } + if (mEphemeralPackages.contains(PackageWithUser.of(userId, packageName))) { + ret.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_EPHEMERAL; + } if (mSystemPackages.contains(packageName)) { ret.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; } diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerPresubmitTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerPresubmitTest.java index 5c552a2437c1..e6b4540f73c9 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerPresubmitTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerPresubmitTest.java @@ -20,7 +20,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; -import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.GlobalPresubmit; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -63,7 +63,7 @@ public class PackageManagerPresubmitTest { */ @Test @SmallTest - @Presubmit + @GlobalPresubmit public void testPrivAppPermissions() throws PackageManager.NameNotFoundException { List<PackageInfo> installedPackages = mPackageManager .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES | GET_PERMISSIONS); @@ -103,12 +103,10 @@ public class PackageManagerPresubmitTest { // if privapp permissions are enforced, platform permissions must be whitelisted // in SystemConfig if (platformPermission && RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE) { - assertTrue("Permission " + pName - + " should be declared in " - + "/etc/permissions/privapp-permissions-platform.xml " - + "or privapp-permissions-<device>.xml file for package " + assertTrue("Permission " + pName + " should be declared in " + + "privapp-permissions-<category>.xml file for package " + packageName, - privAppPermissions.contains(pName)); + privAppPermissions != null && privAppPermissions.contains(pName)); } assertTrue("Permission " + pName + " should be granted to " + packageName, granted); } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index d25923c019ca..562de4148bb1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -46,6 +46,7 @@ import android.test.suitebuilder.annotation.SmallTest; import com.android.frameworks.servicestests.R; import com.android.server.pm.ShortcutService.ConfigConstants; +import com.android.server.pm.ShortcutUser.PackageWithUser; import java.io.File; import java.io.FileWriter; @@ -2037,4 +2038,32 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertFalse(mService.isUserUnlockedL(USER_0)); assertFalse(mService.isUserUnlockedL(USER_10)); } + + public void testEphemeralApp() { + mRunningUsers.put(USER_10, true); // this test needs user 10. + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertWith(mManager.getDynamicShortcuts()).isEmpty(); + }); + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertWith(mManager.getDynamicShortcuts()).isEmpty(); + }); + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertWith(mManager.getDynamicShortcuts()).isEmpty(); + }); + // Make package 1 ephemeral. + mEphemeralPackages.add(PackageWithUser.of(USER_0, CALLING_PACKAGE_1)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertExpectException(IllegalStateException.class, "Ephemeral apps", () -> { + mManager.getDynamicShortcuts(); + }); + }); + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertWith(mManager.getDynamicShortcuts()).isEmpty(); + }); + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertWith(mManager.getDynamicShortcuts()).isEmpty(); + }); + } } diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index 83a61ca99d51..67e78d4a2108 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -19,25 +19,39 @@ package com.android.server.webkit; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; +import android.webkit.UserPackage; import android.webkit.WebViewProviderInfo; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; public class TestSystemImpl implements SystemInterface { private String mUserProvider = null; private final WebViewProviderInfo[] mPackageConfigs; - HashMap<String, PackageInfo> mPackages = new HashMap(); + List<Integer> mUsers = new ArrayList<>(); + // Package -> [user, package] + Map<String, Map<Integer, PackageInfo>> mPackages = new HashMap(); private boolean mFallbackLogicEnabled; private final int mNumRelros; private final boolean mIsDebuggable; private int mMultiProcessSetting; + public static final int PRIMARY_USER_ID = 0; + public TestSystemImpl(WebViewProviderInfo[] packageConfigs, boolean fallbackLogicEnabled, int numRelros, boolean isDebuggable) { mPackageConfigs = packageConfigs; mFallbackLogicEnabled = fallbackLogicEnabled; mNumRelros = numRelros; mIsDebuggable = isDebuggable; + mUsers.add(PRIMARY_USER_ID); + } + + public void addUser(int userId) { + mUsers.add(userId); } @Override @@ -78,17 +92,20 @@ public class TestSystemImpl implements SystemInterface { @Override public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { - enablePackageForUser(packageName, enable, 0); + for(int userId : mUsers) { + enablePackageForUser(packageName, enable, userId); + } } @Override public void enablePackageForUser(String packageName, boolean enable, int userId) { - PackageInfo packageInfo = mPackages.get(packageName); - if (packageInfo == null) { + Map<Integer, PackageInfo> userPackages = mPackages.get(packageName); + if (userPackages == null) { throw new IllegalArgumentException("There is no package called " + packageName); } + PackageInfo packageInfo = userPackages.get(userId); packageInfo.applicationInfo.enabled = enable; - setPackageInfo(packageInfo); + setPackageInfoForUser(userId, packageInfo); } @Override @@ -97,23 +114,61 @@ public class TestSystemImpl implements SystemInterface { @Override public PackageInfo getPackageInfoForProvider(WebViewProviderInfo info) throws NameNotFoundException { - PackageInfo ret = mPackages.get(info.packageName); + Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName); + if (userPackages == null) throw new NameNotFoundException(info.packageName); + PackageInfo ret = userPackages.get(PRIMARY_USER_ID); if (ret == null) throw new NameNotFoundException(info.packageName); return ret; } + @Override + public List<UserPackage> getPackageInfoForProviderAllUsers( + Context context, WebViewProviderInfo info) { + Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName); + List<UserPackage> ret = new ArrayList(); + // Loop over defined users, and find the corresponding package for each user. + for (int userId : mUsers) { + ret.add(new UserPackage(createUserInfo(userId), + userPackages == null ? null : userPackages.get(userId))); + } + return ret; + } + + private static UserInfo createUserInfo(int userId) { + return new UserInfo(userId, "User nr. " + userId, 0 /* flags */); + } + + /** + * Set package for primary user. + */ public void setPackageInfo(PackageInfo pi) { - mPackages.put(pi.packageName, pi); + setPackageInfoForUser(PRIMARY_USER_ID, pi); + } + + public void setPackageInfoForUser(int userId, PackageInfo pi) { + if (!mUsers.contains(userId)) { + throw new IllegalArgumentException("User nr. " + userId + " doesn't exist"); + } + if (!mPackages.containsKey(pi.packageName)) { + mPackages.put(pi.packageName, new HashMap<Integer, PackageInfo>()); + } + mPackages.get(pi.packageName).put(userId, pi); } + /** + * Removes the package {@param packageName} for the primary user. + */ public void removePackageInfo(String packageName) { - mPackages.remove(packageName); + mPackages.get(packageName).remove(PRIMARY_USER_ID); } @Override public int getFactoryPackageVersion(String packageName) throws NameNotFoundException { PackageInfo pi = null; - pi = mPackages.get(packageName); + Map<Integer, PackageInfo> userPackages = mPackages.get(packageName); + if (userPackages == null) throw new NameNotFoundException(); + + pi = userPackages.get(PRIMARY_USER_ID); if (pi != null && pi.applicationInfo.isSystemApp()) { return pi.applicationInfo.versionCode; } diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index 05194488b08a..33cedfa41e51 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -92,9 +92,15 @@ public class WebViewUpdateServiceTest { } private void setEnabledAndValidPackageInfos(WebViewProviderInfo[] providers) { + // Set package infos for the primary user (user 0). + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, providers); + } + + private void setEnabledAndValidPackageInfosForUser(int userId, + WebViewProviderInfo[] providers) { for(WebViewProviderInfo wpi : providers) { - mTestSystemImpl.setPackageInfo(createPackageInfo(wpi.packageName, true /* enabled */, - true /* valid */, true /* installed */)); + mTestSystemImpl.setPackageInfoForUser(userId, createPackageInfo(wpi.packageName, + true /* enabled */, true /* valid */, true /* installed */)); } } @@ -335,7 +341,7 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfos(packages); mWebViewUpdateServiceImpl.packageStateChanged(singlePackage, - WebViewUpdateService.PACKAGE_ADDED, 0); + WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); checkPreparationPhasesForPackage(singlePackage, 1 /* number of finished preparations */); assertEquals(singlePackage, @@ -344,7 +350,7 @@ public class WebViewUpdateServiceTest { // Remove the package again mTestSystemImpl.removePackageInfo(singlePackage); mWebViewUpdateServiceImpl.packageStateChanged(singlePackage, - WebViewUpdateService.PACKAGE_ADDED, 0); + WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); // Package removed - ensure our interface states that there is no package response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); @@ -374,7 +380,7 @@ public class WebViewUpdateServiceTest { createPackageInfo(wpi.packageName, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(wpi.packageName, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); checkPreparationPhasesForPackage(wpi.packageName, 1); } @@ -429,16 +435,8 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(firstPackage, "", true, false, null), new WebViewProviderInfo(secondPackage, "", true, false, null)}; setupWithPackages(packages); - if (settingsChange) { - // Have all packages be enabled, so that we can change provider however we want to - setEnabledAndValidPackageInfos(packages); - } else { - // Have all packages be disabled so that we can change one to enabled later - for(WebViewProviderInfo wpi : packages) { - mTestSystemImpl.setPackageInfo(createPackageInfo(wpi.packageName, - false /* enabled */, true /* valid */, true /* installed */)); - } - } + // Have all packages be enabled, so that we can change provider however we want to + setEnabledAndValidPackageInfos(packages); CountDownLatch countdown = new CountDownLatch(1); @@ -457,8 +455,12 @@ public class WebViewUpdateServiceTest { mWebViewUpdateServiceImpl.waitForAndGetProvider(); assertEquals(WebViewFactory.LIBLOAD_SUCCESS, threadResponse.status); assertEquals(secondPackage, threadResponse.packageInfo.packageName); - // Verify that we killed the first package - Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage)); + // Verify that we killed the first package if we performed a settings change - + // otherwise we had to disable the first package, in which case its dependents + // should have been killed by the framework. + if (settingsChange) { + Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage)); + } countdown.countDown(); } }).start(); @@ -470,11 +472,21 @@ public class WebViewUpdateServiceTest { if (settingsChange) { mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage); } else { - // Switch provider by enabling the second one + // Enable the second provider mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged( - secondPackage, WebViewUpdateService.PACKAGE_CHANGED, 0); + secondPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); + + // Ensure we haven't changed package yet. + assertEquals(firstPackage, + mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName); + + // Switch provider by disabling the first one + mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, false /* enabled */, + true /* valid */, true /* installed */)); + mWebViewUpdateServiceImpl.packageStateChanged( + firstPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); } mWebViewUpdateServiceImpl.notifyRelroCreationCompleted(); // first package done, should start on second @@ -528,7 +540,7 @@ public class WebViewUpdateServiceTest { mTestSystemImpl.setPackageInfo(createPackageInfo(fallbackPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged( - fallbackPackage, WebViewUpdateService.PACKAGE_CHANGED, 0); + fallbackPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); if (fallbackLogicEnabled) { // Check that we have now disabled the fallback package twice @@ -573,7 +585,7 @@ public class WebViewUpdateServiceTest { createPackageInfo(primaryPackage, true /* enabled */ , true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); // Verify fallback disabled, primary package used as provider, and fallback package killed Mockito.verify(mTestSystemImpl).uninstallAndDisablePackageForAllUsers( @@ -583,7 +595,31 @@ public class WebViewUpdateServiceTest { } @Test - public void testFallbackChangesEnabledState() { + public void testFallbackChangesEnabledStateSingleUser() { + for (PackageRemovalType removalType : REMOVAL_TYPES) { + checkFallbackChangesEnabledState(false /* multiUser */, removalType); + } + } + + @Test + public void testFallbackChangesEnabledStateMultiUser() { + for (PackageRemovalType removalType : REMOVAL_TYPES) { + checkFallbackChangesEnabledState(true /* multiUser */, removalType); + } + } + + /** + * Represents how to remove a package during a tests (disabling it / uninstalling it / hiding + * it). + */ + private enum PackageRemovalType { + UNINSTALL, DISABLE, HIDE + } + + private PackageRemovalType[] REMOVAL_TYPES = PackageRemovalType.class.getEnumConstants(); + + public void checkFallbackChangesEnabledState(boolean multiUser, + PackageRemovalType removalType) { String primaryPackage = "primary"; String fallbackPackage = "fallback"; WebViewProviderInfo[] packages = new WebViewProviderInfo[] { @@ -592,46 +628,68 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo( fallbackPackage, "", true /* default available */, true /* fallback */, null)}; setupWithPackages(packages, true /* fallbackLogicEnabled */); - setEnabledAndValidPackageInfos(packages); + int secondaryUserId = 10; + int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID; + if (multiUser) { + mTestSystemImpl.addUser(secondaryUserId); + setEnabledAndValidPackageInfosForUser(secondaryUserId, packages); + } + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); runWebViewBootPreparationOnMainSync(); // Verify fallback disabled at boot when primary package enabled - Mockito.verify(mTestSystemImpl).enablePackageForUser( - Mockito.eq(fallbackPackage), Mockito.eq(false) /* enable */, - Matchers.anyInt()); + checkEnablePackageForUserCalled(fallbackPackage, false, multiUser + ? new int[] {TestSystemImpl.PRIMARY_USER_ID, secondaryUserId} + : new int[] {TestSystemImpl.PRIMARY_USER_ID}, 1 /* numUsages */); checkPreparationPhasesForPackage(primaryPackage, 1); + boolean enabled = !(removalType == PackageRemovalType.DISABLE); + boolean installed = !(removalType == PackageRemovalType.UNINSTALL); + boolean hidden = (removalType == PackageRemovalType.HIDE); // Disable primary package and ensure fallback becomes enabled and used - mTestSystemImpl.setPackageInfo( - createPackageInfo(primaryPackage, false /* enabled */, true /* valid */, - true /* installed */)); + mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor, + createPackageInfo(primaryPackage, enabled /* enabled */, true /* valid */, + installed /* installed */, null /* signature */, 0 /* updateTime */, + hidden /* hidden */)); mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, - WebViewUpdateService.PACKAGE_CHANGED, 0); + removalType == PackageRemovalType.DISABLE + ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_REMOVED, + userIdToChangePackageFor); // USER ID - Mockito.verify(mTestSystemImpl).enablePackageForUser( - Mockito.eq(fallbackPackage), Mockito.eq(true) /* enable */, - Matchers.anyInt()); + checkEnablePackageForUserCalled(fallbackPackage, true, multiUser + ? new int[] {TestSystemImpl.PRIMARY_USER_ID, secondaryUserId} + : new int[] {TestSystemImpl.PRIMARY_USER_ID}, 1 /* numUsages */); checkPreparationPhasesForPackage(fallbackPackage, 1); // Again enable primary package and verify primary is used and fallback becomes disabled - mTestSystemImpl.setPackageInfo( + mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor, createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, - WebViewUpdateService.PACKAGE_CHANGED, 0); + removalType == PackageRemovalType.DISABLE + ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_ADDED, + userIdToChangePackageFor); // Verify fallback is disabled a second time when primary package becomes enabled - Mockito.verify(mTestSystemImpl, Mockito.times(2)).enablePackageForUser( - Mockito.eq(fallbackPackage), Mockito.eq(false) /* enable */, - Matchers.anyInt()); + checkEnablePackageForUserCalled(fallbackPackage, false, multiUser + ? new int[] {TestSystemImpl.PRIMARY_USER_ID, secondaryUserId} + : new int[] {TestSystemImpl.PRIMARY_USER_ID}, 2 /* numUsages */); checkPreparationPhasesForPackage(primaryPackage, 2); } + private void checkEnablePackageForUserCalled(String packageName, boolean expectEnabled, + int[] userIds, int numUsages) { + for (int userId : userIds) { + Mockito.verify(mTestSystemImpl, Mockito.times(numUsages)).enablePackageForUser( + Mockito.eq(packageName), Mockito.eq(expectEnabled), Mockito.eq(userId)); + } + } + @Test public void testAddUserWhenFallbackLogicEnabled() { checkAddingNewUser(true); @@ -651,8 +709,10 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo( fallbackPackage, "", true /* default available */, true /* fallback */, null)}; setupWithPackages(packages, fallbackLogicEnabled); - setEnabledAndValidPackageInfos(packages); + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); int newUser = 100; + mTestSystemImpl.addUser(newUser); + setEnabledAndValidPackageInfosForUser(newUser, packages); mWebViewUpdateServiceImpl.handleNewUser(newUser); if (fallbackLogicEnabled) { // Verify fallback package becomes disabled for new user @@ -668,6 +728,42 @@ public class WebViewUpdateServiceTest { } /** + * Ensures that adding a new user for which the current WebView package is uninstalled causes a + * change of WebView provider. + */ + @Test + public void testAddingNewUserWithUninstalledPackage() { + String primaryPackage = "primary"; + String fallbackPackage = "fallback"; + WebViewProviderInfo[] packages = new WebViewProviderInfo[] { + new WebViewProviderInfo( + primaryPackage, "", true /* default available */, false /* fallback */, null), + new WebViewProviderInfo( + fallbackPackage, "", true /* default available */, true /* fallback */, null)}; + setupWithPackages(packages, true /* fallbackLogicEnabled */); + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); + int newUser = 100; + mTestSystemImpl.addUser(newUser); + // Let the primary package be uninstalled for the new user + mTestSystemImpl.setPackageInfoForUser(newUser, + createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, + false /* installed */)); + mTestSystemImpl.setPackageInfoForUser(newUser, + createPackageInfo(fallbackPackage, false /* enabled */, true /* valid */, + true /* installed */)); + mWebViewUpdateServiceImpl.handleNewUser(newUser); + // Verify fallback package doesn't become disabled for the primary user. + Mockito.verify(mTestSystemImpl, Mockito.never()).enablePackageForUser( + Mockito.anyObject(), Mockito.eq(false) /* enable */, + Mockito.eq(TestSystemImpl.PRIMARY_USER_ID) /* user */); + // Verify that we enable the fallback package for the secondary user. + Mockito.verify(mTestSystemImpl, Mockito.times(1)).enablePackageForUser( + Mockito.eq(fallbackPackage), Mockito.eq(true) /* enable */, + Mockito.eq(newUser) /* user */); + checkPreparationPhasesForPackage(fallbackPackage, 1 /* numRelros */); + } + + /** * Timing dependent test where we verify that the list of valid webview packages becoming empty * at a certain point doesn't crash us or break our state. */ @@ -713,7 +809,7 @@ public class WebViewUpdateServiceTest { 1 /* updateTime */ )); mWebViewUpdateServiceImpl.packageStateChanged(firstPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); // Ensure we use firstPackage checkPreparationPhasesForPackage(firstPackage, 2 /* second preparation for this package */); @@ -742,14 +838,14 @@ public class WebViewUpdateServiceTest { mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */, false /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, - WebViewUpdateService.PACKAGE_ADDED, 0); + WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); checkPreparationPhasesForPackage(firstPackage, 2 /* second time for this package */); // Now make the second package valid again and verify that it is used again mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, - WebViewUpdateService.PACKAGE_ADDED, 0); + WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); checkPreparationPhasesForPackage(secondPackage, 2 /* second time for this package */); } @@ -820,7 +916,7 @@ public class WebViewUpdateServiceTest { mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage); } else { mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); } WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); @@ -831,7 +927,7 @@ public class WebViewUpdateServiceTest { true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); checkPreparationPhasesForPackage(secondPackage, 1); @@ -863,11 +959,11 @@ public class WebViewUpdateServiceTest { createPackageInfo(firstPackage, true /* enabled */, false /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged(firstPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, 0); + WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); } else { mTestSystemImpl.removePackageInfo(firstPackage); mWebViewUpdateServiceImpl.packageStateChanged(firstPackage, - WebViewUpdateService.PACKAGE_REMOVED, 0); + WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID); } checkPreparationPhasesForPackage(secondPackage, 1); @@ -1098,8 +1194,10 @@ public class WebViewUpdateServiceTest { } } - // Ensure that the update service uses an uninstalled package if that is the only package - // available. + /** + * Ensure that the update service does use an uninstalled package when that is the only + * package available. + */ @Test public void testWithSingleUninstalledPackage() { String testPackageName = "test.package.name"; @@ -1113,21 +1211,32 @@ public class WebViewUpdateServiceTest { runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(testPackageName, 1 /* first preparation phase */); + // TODO(gsennton) change this logic to use the code below when we have created a functional + // stub. + //Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged( + // Matchers.anyObject()); + //WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); + //assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status); + //assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage()); } @Test public void testNonhiddenPackageUserOverHidden() { - checkVisiblePackageUserOverNonVisible(false /* true == uninstalled, false == hidden */); + checkVisiblePackageUserOverNonVisible(false /* multiUser*/, PackageRemovalType.HIDE); + checkVisiblePackageUserOverNonVisible(true /* multiUser*/, PackageRemovalType.HIDE); } @Test public void testInstalledPackageUsedOverUninstalled() { - checkVisiblePackageUserOverNonVisible(true /* true == uninstalled, false == hidden */); + checkVisiblePackageUserOverNonVisible(false /* multiUser*/, PackageRemovalType.UNINSTALL); + checkVisiblePackageUserOverNonVisible(true /* multiUser*/, PackageRemovalType.UNINSTALL); } - private void checkVisiblePackageUserOverNonVisible(boolean uninstalledNotHidden) { - boolean testUninstalled = uninstalledNotHidden; - boolean testHidden = !uninstalledNotHidden; + private void checkVisiblePackageUserOverNonVisible(boolean multiUser, + PackageRemovalType removalType) { + assert removalType != PackageRemovalType.DISABLE; + boolean testUninstalled = removalType == PackageRemovalType.UNINSTALL; + boolean testHidden = removalType == PackageRemovalType.HIDE; String installedPackage = "installedPackage"; String uninstalledPackage = "uninstalledPackage"; WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { @@ -1137,11 +1246,25 @@ public class WebViewUpdateServiceTest { false /* fallback */, null)}; setupWithPackages(webviewPackages, true /* fallback logic enabled */, 1 /* numRelros */); - mTestSystemImpl.setPackageInfo(createPackageInfo(installedPackage, true /* enabled */, + int secondaryUserId = 5; + if (multiUser) { + mTestSystemImpl.addUser(secondaryUserId); + // Install all packages for the primary user. + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages); + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo( + installedPackage, true /* enabled */, true /* valid */, true /* installed */)); + // Hide or uninstall the primary package for the second user + mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, + true /* valid */, (testUninstalled ? false : true) /* installed */, + null /* signatures */, 0 /* updateTime */, (testHidden ? true : false))); + } else { + mTestSystemImpl.setPackageInfo(createPackageInfo(installedPackage, true /* enabled */, true /* valid */, true /* installed */)); - mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, + // Hide or uninstall the primary package + mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, true /* valid */, (testUninstalled ? false : true) /* installed */, null /* signatures */, 0 /* updateTime */, (testHidden ? true : false))); + } runWebViewBootPreparationOnMainSync(); @@ -1160,9 +1283,7 @@ public class WebViewUpdateServiceTest { } /** - * Ensure that we won't prioritize an uninstalled (or hidden) package even if it is user-chosen, - * and that an uninstalled (or hidden) package is not considered valid (in the - * getValidWebViewPackages() API). + * Ensure that we won't prioritize an uninstalled (or hidden) package even if it is user-chosen. */ private void checkCantSwitchToNonVisiblePackage(boolean uninstalledNotHidden) { boolean testUninstalled = uninstalledNotHidden; @@ -1176,27 +1297,31 @@ public class WebViewUpdateServiceTest { false /* fallback */, null)}; setupWithPackages(webviewPackages, true /* fallback logic enabled */, 1 /* numRelros */); - mTestSystemImpl.setPackageInfo(createPackageInfo(installedPackage, true /* enabled */, - true /* valid */, true /* installed */)); - mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, - true /* valid */, (testUninstalled ? false : true) /* installed */, - null /* signatures */, 0 /* updateTime */, - (testHidden ? true : false) /* hidden */)); + int secondaryUserId = 412; + mTestSystemImpl.addUser(secondaryUserId); + + // Let all packages be installed and enabled for the primary user. + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages); + // Only uninstall the 'uninstalled package' for the secondary user. + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(installedPackage, + true /* enabled */, true /* valid */, true /* installed */)); + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(uninstalledPackage, + true /* enabled */, true /* valid */, !testUninstalled /* installed */, + null /* signatures */, 0 /* updateTime */, testHidden /* hidden */)); runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(installedPackage, 1 /* first preparation phase */); - // Ensure that only the installed package is considered valid - WebViewProviderInfo[] validPackages = mWebViewUpdateServiceImpl.getValidWebViewPackages(); - assertEquals(1, validPackages.length); - assertEquals(installedPackage, validPackages[0].packageName); - // ensure that we don't switch to the uninstalled package (it will be used if it becomes // installed later) assertEquals(installedPackage, mWebViewUpdateServiceImpl.changeProviderAndSetting(uninstalledPackage)); + // Ensure both packages are considered valid. + assertEquals(2, mWebViewUpdateServiceImpl.getValidWebViewPackages().length); + + // We should only have called onWebViewProviderChanged once (before calling // changeProviderAndSetting Mockito.verify(mTestSystemImpl, Mockito.times(1)).onWebViewProviderChanged( @@ -1227,12 +1352,16 @@ public class WebViewUpdateServiceTest { false /* fallback */, null)}; setupWithPackages(webviewPackages, true /* fallback logic enabled */, 1 /* numRelros */); - mTestSystemImpl.setPackageInfo(createPackageInfo(installedPackage, true /* enabled */, - true /* valid */, true /* installed */)); - mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, - true /* valid */, (testUninstalled ? false : true) /* installed */, - null /* signatures */, 0 /* updateTime */, - (testHidden ? true : false) /* hidden */)); + int secondaryUserId = 4; + mTestSystemImpl.addUser(secondaryUserId); + + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages); + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(installedPackage, + true /* enabled */, true /* valid */, true /* installed */)); + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(uninstalledPackage, + true /* enabled */, true /* valid */, + (testUninstalled ? false : true) /* installed */, null /* signatures */, + 0 /* updateTime */, (testHidden ? true : false) /* hidden */)); // Start with the setting pointing to the uninstalled package mTestSystemImpl.updateUserSetting(null, uninstalledPackage); @@ -1242,12 +1371,21 @@ public class WebViewUpdateServiceTest { checkPreparationPhasesForPackage(installedPackage, 1 /* first preparation phase */); } + @Test + public void testFallbackEnabledIfPrimaryUninstalledSingleUser() { + checkFallbackEnabledIfPrimaryUninstalled(false /* multiUser */); + } + + @Test + public void testFallbackEnabledIfPrimaryUninstalledMultiUser() { + checkFallbackEnabledIfPrimaryUninstalled(true /* multiUser */); + } + /** - * Ensures that fallback becomes enabled if the primary package is uninstalled for the current + * Ensures that fallback becomes enabled at boot if the primary package is uninstalled for some * user. */ - @Test - public void testFallbackEnabledIfPrimaryUninstalled() { + private void checkFallbackEnabledIfPrimaryUninstalled(boolean multiUser) { String primaryPackage = "primary"; String fallbackPackage = "fallback"; WebViewProviderInfo[] packages = new WebViewProviderInfo[] { @@ -1256,10 +1394,24 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo( fallbackPackage, "", true /* default available */, true /* fallback */, null)}; setupWithPackages(packages, true /* fallback logic enabled */); - mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */, + int secondaryUserId = 5; + if (multiUser) { + mTestSystemImpl.addUser(secondaryUserId); + // Install all packages for the primary user. + setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); + // Only install fallback package for secondary user. + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, + createPackageInfo(primaryPackage, true /* enabled */, + true /* valid */, false /* installed */)); + mTestSystemImpl.setPackageInfoForUser(secondaryUserId, + createPackageInfo(fallbackPackage, false /* enabled */, + true /* valid */, true /* installed */)); + } else { + mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, false /* installed */)); - mTestSystemImpl.setPackageInfo(createPackageInfo(fallbackPackage, true /* enabled */, + mTestSystemImpl.setPackageInfo(createPackageInfo(fallbackPackage, false /* enabled */, true /* valid */, true /* installed */)); + } runWebViewBootPreparationOnMainSync(); // Verify that we enable the fallback package diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index db7a31ac33af..fbbe636e6220 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -656,6 +656,7 @@ public class UsbDeviceManager { // send a sticky broadcast containing current USB state Intent intent = new Intent(UsbManager.ACTION_USB_STATE); intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING + | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND | Intent.FLAG_RECEIVER_FOREGROUND); intent.putExtra(UsbManager.USB_CONNECTED, mConnected); intent.putExtra(UsbManager.USB_HOST_CONNECTED, mHostConnected); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 00e8f9f684c9..ba7b6a174a32 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -58,12 +58,15 @@ public class TelecomManager { * Input: get*Extra field {@link #EXTRA_PHONE_ACCOUNT_HANDLE} contains the component name of the * {@link android.telecom.ConnectionService} that Telecom should bind to. Telecom will then * ask the connection service for more information about the call prior to showing any UI. + * + * @deprecated Use {@link #addNewIncomingCall} instead. */ public static final String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; /** * Similar to {@link #ACTION_INCOMING_CALL}, but is used only by Telephony to add a new * sim-initiated MO call for carrier testing. + * @deprecated Use {@link #addNewUnknownCall} instead. * @hide */ public static final String ACTION_NEW_UNKNOWN_CALL = "android.telecom.action.NEW_UNKNOWN_CALL"; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index c0a86d6d3ece..a853d5c3a7f7 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -598,6 +598,38 @@ public class CarrierConfigManager { "vvm_cellular_data_required_bool"; /** + * The default OMTP visual voicemail client prefix to use. Defaulted to "//VVM" + */ + public static final String KEY_VVM_CLIENT_PREFIX_STRING = + "vvm_client_prefix_string"; + + /** + * Whether to use SSL to connect to the visual voicemail IMAP server. Defaulted to false. + */ + public static final String KEY_VVM_SSL_ENABLED_BOOL = "vvm_ssl_enabled_bool"; + + /** + * A set of capabilities that should not be used even if it is reported by the visual voicemail + * IMAP CAPABILITY command. + */ + public static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY = + "vvm_disabled_capabilities_string_array"; + + /** + * Whether legacy mode should be used when the visual voicemail client is disabled. + * + * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on + * the client side all network operations are disabled. SMSs are still monitored so a new + * message SYNC SMS will be translated to show a message waiting indicator, like traditional + * voicemails. + * + * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to + * function without the data cost. + */ + public static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = + "vvm_legacy_mode_enabled_bool"; + + /** * Whether to prefetch audio data on new voicemail arrival, defaulted to true. */ public static final String KEY_VVM_PREFETCH_BOOL = "vvm_prefetch_bool"; @@ -605,10 +637,20 @@ public class CarrierConfigManager { /** * The package name of the carrier's visual voicemail app to ensure that dialer visual voicemail * and carrier visual voicemail are not active at the same time. + * + * @deprecated use {@link #KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY}. */ + @Deprecated public static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = "carrier_vvm_package_name_string"; /** + * A list of the carrier's visual voicemail app package names to ensure that dialer visual + * voicemail and carrier visual voicemail are not active at the same time. + */ + public static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY = + "carrier_vvm_package_name_string_array"; + + /** * Flag specifying whether ICCID is showed in SIM Status screen, default to false. */ public static final String KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL = "show_iccid_in_sim_status_bool"; @@ -1308,8 +1350,13 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_VVM_PORT_NUMBER_INT, 0); sDefaults.putString(KEY_VVM_TYPE_STRING, ""); sDefaults.putBoolean(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false); + sDefaults.putString(KEY_VVM_CLIENT_PREFIX_STRING,"//VVM"); + sDefaults.putBoolean(KEY_VVM_SSL_ENABLED_BOOL,false); + sDefaults.putStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY, null); + sDefaults.putBoolean(KEY_VVM_LEGACY_MODE_ENABLED_BOOL,false); sDefaults.putBoolean(KEY_VVM_PREFETCH_BOOL, true); sDefaults.putString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING, ""); + sDefaults.putStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY, null); sDefaults.putBoolean(KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL, false); sDefaults.putBoolean(KEY_CI_ACTION_ON_SYS_UPDATE_BOOL, false); sDefaults.putString(KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING, ""); diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java index 50efc7f7db86..d0c959972b13 100644 --- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java @@ -16,6 +16,7 @@ package android.graphics; +import android.text.FontConfig; import com.android.ide.common.rendering.api.AssetRepository; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; @@ -284,7 +285,7 @@ public class FontFamily_Delegate { @LayoutlibDelegate /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font, - int ttcIndex, List<FontListParser.Axis> listOfAxis, + int ttcIndex, List<FontConfig.Axis> listOfAxis, int weight, boolean isItalic) { assert false : "The only client of this method has been overriden."; return false; diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java index 5cd34f6e000f..6e337d50a31f 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java @@ -16,6 +16,7 @@ package android.graphics; +import android.text.FontConfig; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; @@ -208,12 +209,12 @@ public final class Typeface_Delegate { } @LayoutlibDelegate - /*package*/ static FontFamily makeFamilyFromParsed(FontListParser.Family family, + /*package*/ static FontFamily makeFamilyFromParsed(FontConfig.Family family, Map<String, ByteBuffer> bufferForPath) { - FontFamily fontFamily = new FontFamily(family.lang, family.variant); - for (FontListParser.Font font : family.fonts) { - FontFamily_Delegate.addFont(fontFamily.mNativePtr, font.fontName, font.weight, - font.isItalic); + FontFamily fontFamily = new FontFamily(family.getLanguage(), family.getVariant()); + for (FontConfig.Font font : family.getFonts()) { + FontFamily_Delegate.addFont(fontFamily.mNativePtr, font.getFontName(), + font.getWeight(), font.isItalic()); } return fontFamily; } |