diff --git a/Android.bp b/Android.bp index e07d69d045..0a55675d43 100644 --- a/Android.bp +++ b/Android.bp @@ -31,11 +31,13 @@ android_library { "androidx.test.uiautomator_uiautomator", "androidx.preference_preference", "SystemUISharedLib", + "SystemUIAnimationLib", ], srcs: [ "tests/tapl/**/*.java", "src/com/android/launcher3/ResourceUtils.java", "src/com/android/launcher3/testing/TestProtocol.java", + "src/com/android/launcher3/testing/*Request.java", ], resource_dirs: [ ], manifest: "tests/tapl/AndroidManifest.xml", @@ -195,6 +197,7 @@ android_library { "lottie", "SystemUISharedLib", "SystemUI-statsd", + "SystemUIAnimationLib", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", @@ -207,6 +210,8 @@ filegroup { srcs: [ "ext_tests/src/**/*.java", "ext_tests/src/**/*.kt", + "quickstep/ext_tests/src/**/*.java", + "quickstep/ext_tests/src/**/*.kt", ], } @@ -223,6 +228,19 @@ filegroup { ], } +// Common source files used to build go launcher except go/src files +filegroup { + name: "launcher-go-src-no-build-config", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + "quickstep/src/**/*.java", + "quickstep/src/**/*.kt", + "go/quickstep/src/**/*.java", + "go/quickstep/src/**/*.kt", + ], +} + // Proguard files for Launcher3 filegroup { name: "launcher-proguard-rules", @@ -253,7 +271,9 @@ android_library { static_libs: [ "Launcher3CommonDepsLib", "QuickstepResLib", + "androidx.room_room-runtime", ], + plugins: ["androidx.room_room-compiler-plugin"], manifest: "quickstep/AndroidManifest-launcher.xml", additional_manifests: [ "go/AndroidManifest.xml", @@ -284,6 +304,7 @@ android_library { "SystemUISharedLib", "Launcher3CommonDepsLib", "QuickstepResLib", + "SystemUIAnimationLib", ], manifest: "quickstep/AndroidManifest.xml", platform_apis: true, diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 3fd7375c3f..02b83fe889 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -42,6 +42,7 @@ + diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 398489044e..4f580e0bd6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,7 +20,7 @@ - + + + + 200 + \ No newline at end of file diff --git a/go/quickstep/res/values/strings.xml b/go/quickstep/res/values/strings.xml index 8429f6a7f2..42f4702141 100644 --- a/go/quickstep/res/values/strings.xml +++ b/go/quickstep/res/values/strings.xml @@ -41,4 +41,8 @@ Tap here to listen to text on this screen Tap here to translate text on this screen + + + + This app can’t be shared diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml index 442c41375d..c659331bde 100644 --- a/go/quickstep/res/values/styles.xml +++ b/go/quickstep/res/values/styles.xml @@ -73,7 +73,7 @@ + + + + @@ -77,8 +89,8 @@ + + + \ No newline at end of file diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 6972ee325e..6a9a9b3f2c 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -37,8 +37,8 @@ "%1$d × %2$d" "Rộng %1$d x cao %2$d" "Tiện ích %1$s" - "Chạm và giữ để di chuyển tiện ích xung quanh Màn hình chính" - "Thêm vào Màn hình chính" + "Chạm và giữ tiện ích để di chuyển tiện ích đó xung quanh màn hình chính" + "Thêm vào màn hình chính" "Đã thêm tiện ích %1$s vào màn hình chính" "{count,plural, =1{# tiện ích}other{# tiện ích}}" "{count,plural, =1{# lối tắt}other{# lối tắt}}" @@ -52,7 +52,7 @@ "Công việc" "Cuộc trò chuyện" "Thông tin hữu ích ngay trong tầm tay bạn" - "Để nhận thông tin mà không cần mở các ứng dụng, bạn có thể thêm tiện ích vào Màn hình chính" + "Để nhận thông tin mà không cần mở các ứng dụng, bạn có thể thêm tiện ích vào màn hình chính" "Nhấn để thay đổi chế độ cài đặt tiện ích" "Tôi hiểu" "Thay đổi chế độ cài đặt tiện ích" @@ -65,7 +65,7 @@ "Thông báo" "Chạm và giữ để di chuyển một lối tắt." "Nhấn đúp và giữ để di chuyển một lối tắt hoặc sử dụng các thao tác tùy chỉnh." - "Không còn khoảng trống trên Màn hình chính này" + "Không còn khoảng trống trên màn hình chính này" "Không còn chỗ trong khay Mục yêu thích" "Danh sách ứng dụng" "Kết quả tìm kiếm" @@ -79,10 +79,10 @@ "Ghim ứng dụng dự đoán" "cài đặt lối tắt" "Cho phép ứng dụng thêm lối tắt mà không cần sự can thiệp của người dùng." - "đọc cài đặt và lối tắt trên Màn hình chính" - "Cho phép ứng dụng đọc cài đặt và lối tắt trên Màn hình chính." - "ghi cài đặt và lối tắt trên Màn hình chính" - "Cho phép ứng dụng thay đổi cài đặt và lối tắt trên Màn hình chính." + "đọc lối tắt và các chế độ cài đặt trên màn hình chính" + "Cho phép ứng dụng đọc các chế độ cài đặt và lối tắt trên màn hình chính." + "ghi lối tắt và các chế độ cài đặt trên màn hình chính" + "Cho phép ứng dụng thay đổi các chế độ cài đặt và lối tắt trên màn hình chính." "%1$s không được phép thực hiện cuộc gọi điện thoại" "Không thể tải tiện ích" "Cài đặt tiện ích" @@ -90,7 +90,7 @@ "Đây là ứng dụng hệ thống và không thể gỡ cài đặt." "Chỉnh sửa tên" "Đã vô hiệu hóa %1$s" - "{count,plural,offset:1 =1{{app_name} có # thông báo}other{{app_name} có # thông báo}}" + "{count,plural, =1{{app_name} có # thông báo}other{{app_name} có # thông báo}}" "Trang %1$d / %2$d" "Màn hình chính %1$d / %2$d" "Trang màn hình chính mới" @@ -105,7 +105,7 @@ "Hình nền và phong cách" "Cài đặt màn hình chính" "Bị tắt bởi quản trị viên của bạn" - "Cho phép xoay Màn hình chính" + "Cho phép xoay màn hình chính" "Khi xoay điện thoại" "Dấu chấm thông báo" "Đang bật" @@ -114,7 +114,7 @@ "Để hiển thị Dấu chấm thông báo, hãy bật thông báo ứng dụng cho %1$s" "Thay đổi cài đặt" "Hiện dấu chấm thông báo" - "Thêm biểu tượng ứng dụng vào Màn hình chính" + "Thêm biểu tượng ứng dụng vào màn hình chính" "Cho ứng dụng mới" "Không xác định" "Xóa" @@ -124,9 +124,17 @@ "Đang cài đặt %1$s, hoàn tất %2$s" "Đang tải xuống %1$s, %2$s hoàn tất" "Đang chờ cài đặt %1$s" + + + + + + + + "Danh sách tiện ích" "Đã đóng danh sách tiện ích" - "Thêm vào màn hình chính" + "Thêm vào màn hình chính" "Di chuyển mục vào đây" "Đã thêm mục vào màn hình chính" "Đã xóa mục" @@ -141,7 +149,7 @@ "Đã thêm mục vào thư mục" "Tạo thư mục bằng: %1$s" "Đã tạo thư mục" - "Di chuyển đến màn hình chính" + "Di chuyển đến màn hình chính" "Đổi kích thước" "Tăng chiều rộng" "Tăng chiều cao" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index f11f020f94..ea7d37e605 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -32,13 +32,13 @@ "左分屏" "右分屏" "%1$s 的应用信息" - "轻触并按住即可移动微件。" + "轻触并按住微件即可移动该微件。" "点按两次并按住微件即可移动该微件或使用自定义操作。" "%1$d × %2$d" "宽 %1$d,高 %2$d" "“%1$s”微件" - "轻触并按住该微件即可将其在主屏幕上四处移动" - "添加到主屏幕" + "轻触并按住此微件即可在主屏幕上随意移动它" + "添加到主屏幕" "已将“%1$s”微件添加到主屏幕" "{count,plural, =1{# 个微件}other{# 个微件}}" "{count,plural, =1{# 个快捷方式}other{# 个快捷方式}}" @@ -52,7 +52,7 @@ "工作" "对话" "实用信息触手可及" - "要想不打开应用就能获取信息,您可以将相应微件添加到主屏幕" + "要想不打开应用就能获取信息,您可以将相应微件添加到主屏幕" "点按即可更改微件设置" "知道了" "更改微件设置" @@ -65,7 +65,7 @@ "通知" "轻触并按住快捷方式即可移动该快捷方式。" "点按两次并按住快捷方式即可移动该快捷方式或使用自定义操作。" - "此主屏幕上已没有空间" + "此主屏幕上已没有空间" "收藏栏已满" "应用列表" "搜索结果" @@ -79,10 +79,10 @@ "固定预测的应用" "安装快捷方式" "允许应用自行添加快捷方式。" - "读取主屏幕设置和快捷方式" - "允许应用读取主屏幕中的设置和快捷方式。" - "写入主屏幕设置和快捷方式" - "允许应用更改主屏幕中的设置和快捷方式。" + "读取主屏幕设置和快捷方式" + "允许此应用读取主屏幕中的设置和快捷方式。" + "写入主屏幕设置和快捷方式" + "允许此应用更改主屏幕中的设置和快捷方式。" "不允许使用“%1$s”拨打电话" "无法加载微件" "微件设置" @@ -90,7 +90,7 @@ "这是系统应用,无法卸载。" "修改名称" "已停用%1$s" - "{count,plural,offset:1 =1{“{app_name}”有 # 条通知}other{“{app_name}”有 # 条通知}}" + "{count,plural, =1{“{app_name}”有 # 条通知}other{“{app_name}”有 # 条通知}}" "第%1$d页,共%2$d页" "主屏幕:第%1$d屏,共%2$d屏" "主屏幕新页面" @@ -105,7 +105,7 @@ "壁纸和样式" "主屏幕设置" "已被您的管理员停用" - "允许旋转主屏幕" + "允许旋转主屏幕" "手机旋转时" "通知圆点" "已开启" @@ -114,7 +114,7 @@ "要显示通知圆点,请开启%1$s的应用通知功能" "更改设置" "显示通知圆点" - "将应用图标添加到主屏幕" + "将应用图标添加到主屏幕" "适用于新应用" "未知" "移除" @@ -124,9 +124,17 @@ "正在安装%1$s,已完成 %2$s" "正在下载%1$s,已完成 %2$s" "%1$s正在等待安装" + + + + + + + + "微件列表" "微件列表已关闭" - "添加到主屏幕" + "添加到主屏幕" "将项目移至此处" "已将项目添加到主屏幕" "项目已移除" @@ -141,7 +149,7 @@ "项目已添加到文件夹" "创建“%1$s”文件夹" "文件夹已创建" - "移至主屏幕" + "移至主屏幕" "调整大小" "增加宽度" "增加高度" diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index a278327f75..4e2c060e4d 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -37,8 +37,8 @@ "%1$d × %2$d" "%1$d 闊,%2$d 高" "「%1$s」小工具" - "按住小工具即可隨意在主畫面上移動" - "新增至主畫面" + "按住小工具即可移到主畫面的任何位置" + "加去主畫面" "已經將「%1$s」小工具加咗去主畫面" "{count,plural, =1{# 個小工具}other{# 個小工具}}" "{count,plural, =1{# 個捷徑}other{# 個捷徑}}" @@ -52,7 +52,7 @@ "工作" "對話" "實用資訊,唾手可得" - "將小工具新增到主畫面,不用開啟應用程式就可直接查看資訊" + "只要將小工具新增至主畫面,就可以直接查看資料,無需開啟應用程式" "輕按即可變更小工具設定" "知道了" "變更小工具設定" @@ -65,7 +65,7 @@ "通知" "輕觸並按住即可移動捷徑。" "㩒兩下之後㩒住,就可以郁捷徑或者用自訂操作。" - "這個主畫面沒有空間了" + "主畫面空間已滿" "我的收藏寄存區沒有足夠空間" "應用程式清單" "搜尋結果" @@ -79,10 +79,10 @@ "固定預測" "安裝捷徑" "允許應用程式無需使用者許可也可新增捷徑。" - "讀取主畫面的設定和捷徑" - "允許應用程式讀取主畫面中的設定和捷徑。" - "寫入主畫面的設定和捷徑" - "允許應用程式更改主畫面中的設定和捷徑。" + "讀取主畫面設定和捷徑" + "允許應用程式讀取主畫面中的設定和捷徑。" + "寫入主畫面設定和捷徑" + "允許應用程式變更主畫面中的設定和捷徑。" "不允許 %1$s 撥打電話" "無法載入小工具" "小工具設定" @@ -90,7 +90,7 @@ "這是系統應用程式,無法將其解除安裝。" "編輯名稱" "「%1$s」已停用" - "{count,plural,offset:1 =1{「{app_name}」有 # 項通知}other{「{app_name}」有 # 項通知}}" + "{count,plural, =1{「{app_name}」有 # 項通知}other{「{app_name}」有 # 項通知}}" "第 %1$d 頁,共 %2$d 頁" "主畫面 %1$d,共 %2$d 個" "新主畫面頁面" @@ -105,7 +105,7 @@ "桌布和樣式" "主畫面設定" "已由您的管理員停用" - "允許主畫面旋轉" + "允許旋轉主畫面" "當手機旋轉時" "通知圓點" "開啟" @@ -114,7 +114,7 @@ "如要顯示「通知圓點」,請開啟「%1$s」的應用程式通知功能" "變更設定" "顯示通知圓點" - "將應用程式圖示新增至主畫面" + "將應用程式圖示新增至主畫面" "新安裝的應用程式" "不明" "移除" @@ -124,9 +124,17 @@ "正在安裝「%1$s」(已完成 %2$s)" "正在下載 %1$s,已完成 %2$s" "正在等待安裝 %1$s" + + + + + + + + "小工具清單" "已經關閉嘅小工具清單" - "新增至主畫面" + "加去主畫面" "移動項目至這裡" "已將項目加入至主畫面" "項目已移除" @@ -141,7 +149,7 @@ "項目已加入資料夾" "使用以下項目建立資料夾:%1$s" "已建立資料夾" - "移動至主畫面" + "移去主畫面" "重新調整大小" "增加闊度" "增加高度" diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index a2cfcc780e..8394459be2 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -37,8 +37,8 @@ "%1$d × %2$d" "寬度為 %1$d,高度為 %2$d" "「%1$s」小工具" - "按住小工具即可將它拖放到主畫面上的任何位置" - "新增到主畫面" + "按住小工具即可將它移到主畫面上的任何位置" + "新增至主畫面" "已將「%1$s」小工具新增到主畫面" "{count,plural, =1{# 項小工具}other{# 項小工具}}" "{count,plural, =1{# 個捷徑}other{# 個捷徑}}" @@ -52,7 +52,7 @@ "工作" "對話" "實用資訊隨手可得" - "只要將小工具新增到主畫面,就可以直接查看資訊,不必開啟應用程式" + "只要將小工具新增到主畫面,就可以直接查看資訊,不必開啟應用程式" "輕觸即可變更小工具設定" "我知道了" "變更小工具設定" @@ -65,7 +65,7 @@ "通知" "按住即可移動捷徑。" "輕觸兩下並按住即可移動捷徑或使用自訂操作。" - "這個主畫面已無空間" + "主畫面空間已滿" "「我的最愛」匣已無可用空間" "應用程式清單" "搜尋結果" @@ -79,10 +79,10 @@ "固定預測的應用程式" "安裝捷徑" "允許應用程式自動新增捷徑。" - "讀取主畫面的設定和捷徑" - "允許應用程式讀取主畫面中的設定和捷徑。" - "寫入主畫面設定和捷徑" - "允許應用程式變更主畫面中的設定和捷徑。" + "讀取主畫面設定和捷徑" + "允許應用程式讀取主畫面中的設定和捷徑。" + "寫入主畫面設定和捷徑" + "允許應用程式變更主畫面中的設定和捷徑。" "%1$s 無法撥打電話" "無法載入小工具" "小工具設定" @@ -90,7 +90,7 @@ "這是系統應用程式,不可解除安裝。" "編輯名稱" "已停用 %1$s" - "{count,plural,offset:1 =1{「{app_name}」有 # 則通知}other{「{app_name}」有 # 則通知}}" + "{count,plural, =1{「{app_name}」應用程式有 # 則通知}other{「{app_name}」應用程式有 # 則通知}}" "第 %1$d 頁,共 %2$d 頁" "主畫面:第 %1$d 頁,共 %2$d 頁" "新的主畫面頁面" @@ -105,7 +105,7 @@ "桌布和樣式" "主畫面設定" "已由你的管理員停用" - "允許旋轉主畫面" + "允許旋轉主畫面" "當手機旋轉時" "通知圓點" "開啟" @@ -114,7 +114,7 @@ "如要顯示通知圓點,請開啟「%1$s」的應用程式通知功能" "變更設定" "顯示通知圓點" - "將應用程式圖示加到主畫面" + "將應用程式圖示加到主畫面" "適用於新安裝的應用程式" "不明" "移除" @@ -124,9 +124,17 @@ "正在安裝「%1$s」(已完成 %2$s)" "正在下載「%1$s」,已完成 %2$s" "正在等待安裝「%1$s」" + + + + + + + + "小工具清單" "已關閉小工具清單" - "新增至主畫面" + "新增至主畫面" "將項目移至這裡" "已將項目新增到主畫面" "已移除項目" @@ -141,7 +149,7 @@ "已將項目新增到資料夾" "建立「%1$s」資料夾" "已建立資料夾" - "移至主畫面" + "移至主畫面" "調整大小" "增加寬度" "增加高度" diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml index 1208eb255d..dc378f704b 100644 --- a/res/values-zu/strings.xml +++ b/res/values-zu/strings.xml @@ -37,8 +37,8 @@ "%1$d × %2$d" "%1$d ububanzi ngokungu-%2$d ukuya phezulu" "Iwijethi elingu-%1$s" - "Thinta uphinde ubambe iwijethi ukuyihambisa Kusikrini sasekhaya" - "Engeza kusikrini sasekhaya" + "Thinta uphinde ubambe iwijethi ukuyihambisa kusikrini sasekhaya" + "Faka kusikrini sasekhaya" "Iwijethi ye-%1$s yengezwe kusikrini sasekhaya" "{count,plural, =1{iwijethi #}one{amawijethi #}other{amawijethi #}}" "{count,plural, =1{isinqamuleli #}one{izinqamuleli #}other{izinqamuleli #}}" @@ -52,7 +52,7 @@ "Umsebenzi" "Izingxoxo" "Ulwazi oluwusizo phambi nje kwakho" - "Ukuze utholeulwazi ngaphandle kokuvula ama-app, ungakwazi ukwengeza amawijethi kusikrini sakho sasekhaya" + "Ukuze uthole ulwazi ngaphandle kokuvula ama-app, ungakwazi ukwengeza amawijethi kusikrini sakho sasekhaya" "Thepha ukuze ushintshe amasethingi ewijethi" "Ngiyezwa" "Shintsha amasethingi ewijethi" @@ -65,7 +65,7 @@ "Izaziso" "Thinta uphinde ubambe ukuze uhambise isinqamuleli." "Thepha kabili uphinde ubambe ukuze uhambise isinqamuleli noma usebenzise izenzo ezingokwezifiso." - "Asikho isikhala kulesi sikrini sasekhaya" + "Asikho isikhala kulesi sikrini sasekhaya" "Asisekho isikhala kwitreyi lezintandokazi" "Uhlu lwezinhlelo zokusebenza" "Imiphumela yosesho" @@ -79,10 +79,10 @@ "Ukubikezela Iphinikhodi" "faka izinqamuleli" "Ivumela uhlelo lokusebenza ukufaka izinqamuleli ngaphandle kokungenelela komsebenzisi." - "funda izilungiselelo zokuthi Ikhaya nezinqamuleli" - "Ivumela uhlelo lokusebenza ukuthi lifunde izilungiselelo nezinqamuleli ekhaya." - "bhala izilungiselelo zokuthi Ikhaya nezinqamuleli" - "Ivumela uhlelo lokusebenza ukuthi lushintshe izilungiselelo nezinqamuleli Ekhaya." + "funda amasethingi wasekhaya nezinqamuleli" + "Ivumela i-app ukuthi ifunde amasethingi nezinqamuleli ekhaya." + "bhala amasethingi wasekhaya nezinqamuleli" + "Ivumela ama-app ukushintsha amasethingi nezinqamuleli ekhaya." "%1$s ayivunyelwe ukwenza amakholi wefoni" "Ayikwazi ukulayisha iwijethi" "Amasethingi ewijethi" @@ -90,7 +90,7 @@ "Lolu uhlelo lokusebenza lwesistimu futhi alikwazi ukukhishwa." "Hlela igama" "Kukhutshaziwe %1$s" - "{count,plural,offset:1 =1{I-{app_name}, inesaziso esingu-#}one{I-{app_name}, inezaziso ezingu-#}other{I-{app_name}, inezaziso ezingu-#}}" + "{count,plural, =1{I-{app_name} inesaziso esi-#}one{I-{app_name} inezaziso ezingu-#}other{I-{app_name} inezaziso ezingu-#}}" "Ikhasi elingu-%1$d kwangu-%2$d" "Isikrini sasekhaya esingu-%1$d se-%2$d" "Ikhasi elisha lesikrini sasekhaya" @@ -105,7 +105,7 @@ "Isithombe sangemuva nesitayela" "Amasethingi asekhaya" "Kukhutshazwe umlawuli wakho" - "Vumela ukuphendukiswa kwesikrini sasekhaya" + "Vumela ukuzungezisa kwesikrini sasekhaya" "Uma ifoni iphendukiswa" "Amacashazi esaziso" "Vuliwe" @@ -114,7 +114,7 @@ "Ukuze ubonisa amcashazi esaziso, vula izaziso zohlelo lokusebenza ze-%1$s" "Shintsha izilungiselelo" "Bonisa amacashazi esaziso" - "Engeza izithonjana zohlelo lokusebenza kusikrini sasekhaya" + "Engeza izithonjana ze-app kusikrini sasekhaya" "Kwezinhlelo zokusebenza ezintsha" "Akwaziwa" "Susa" @@ -124,9 +124,17 @@ "I-%1$s iyafakwa, seyiqede %2$s" "I-%1$s iyalandwa, %2$s kuqediwe" "%1$s ilinde ukufakwa" + + + + + + + + "Uhlu lwamawijethi" "Uhlu lwamawijethi luvaliwe" - "Faka kusikrini sasekhaya" + "Faka kusikrini sasekhaya" "Hambisa into lapha" "Into ingezwe kusikrini sasekhaya" "Into isusiwe" @@ -141,7 +149,7 @@ "Into ingeziwe kufolda" "Dala ifolda nge-: %1$s" "Ifolda idaliwe" - "Hambisa kusikrini sasekhaya" + "Hambisa kusikrini sasekhaya" "Shintsha usayizi" "Khuphula ububanzi" "Khuphula ubude" diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 08570eb7c2..13f20c22c4 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -54,6 +54,12 @@ + + + + + + @@ -133,6 +139,8 @@ + + @@ -144,10 +152,24 @@ + + + + + + + + + + + + @@ -182,76 +204,167 @@ - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + - + defaults to borderSpace if not specified --> + - - + + - + - + defaults to borderSpace if not specified --> + - - + + - + - - + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - + + + + + + + + + + + + + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index 0b1b451302..2bc923952d 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -79,4 +79,10 @@ #ff8df5e3 #ff3d665f + + #F7F9FA + #00677E + #00677E + #5F757E + #005A6E diff --git a/res/values/config.xml b/res/values/config.xml index 25911e688f..9aa1f03734 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -1,3 +1,18 @@ + + false @@ -22,12 +37,12 @@ - - 85 - 50 + + 750 + @@ -69,6 +84,7 @@ + @@ -87,7 +103,10 @@ + + com.android.customization.picker.CustomizationPickerActivity + @@ -137,19 +156,9 @@ 0.88 - 4.5 - 3 - - 0.45 - 200 - 0.8 200 - 1 - - 0.85 - 0.7 150 2dp @@ -165,12 +174,6 @@ @dimen/swipe_up_scale_start - @dimen/swipe_up_trans_y_dp - @dimen/swipe_up_trans_y_dp_per_s - @dimen/swipe_up_trans_y_damping - @dimen/swipe_up_trans_y_stiffness - @dimen/swipe_up_launcher_alpha_max_progress - @dimen/swipe_up_low_swipe_duration_multiplier @dimen/swipe_up_max_velocity @@ -179,4 +182,8 @@ + + 10dp + 160dp + 40dp diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 6d223e78f4..8403af46b7 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -19,19 +19,31 @@ 4dp - 8dp + 10.77dp 8dp 7dp 8dp + + 24dp 16dp - 5.5dp + 10.77dp 8dp 8dp 2dp + 0dp + 0dp + 76dp + + + + 0.325 + 0dp 34dp @@ -45,11 +57,18 @@ 1dp 0dp - - 52dp + + 56dp 20dp + 32dp + 16dp - + + + 10sp + 1sp + + 13dp 24dp 22dp @@ -62,7 +81,7 @@ 36dp 16dp - + 6dp 8dp 1dp @@ -83,9 +102,18 @@ 58dp -26dp + + 500dp + 400dp + 250dp + + 1500dp + - 320dp + 300dp 48dp + + 24dp 30dp 40dp 144dp @@ -95,8 +123,11 @@ 12dp 48dp 2dp + 33dp 36dp - 6dp + 14dp + 6dp + 4dp 16dp 20dp 4dp @@ -106,33 +137,46 @@ 150dp 8dp 6dp + 0dp + 40dp + 16dp + 8dp 2dp 2dp 10dp - + 3dp 16dp 6dp 8dp - + 56dp - 28dp - 24dp + 16dp + 10dp 52dp 16dp + 20dp + 16dp 20dp 16dp - 28dp + 16dp + 26dp + + 24dp + + 24dp + 16dp + 4dp 48dp - 32dp - 16dp + 48dp + 200dp 8dp @@ -181,19 +225,23 @@ 0dp 0dp - + 8dp 4dp 4dp 6dp - + 14dp 16sp 2dp 4dp 8dp + 16dp + 8dp + 28dp + 0dp 30dp @@ -207,7 +255,7 @@ 2dp 4dp - + 8dp 9dp @@ -219,22 +267,22 @@ 8dp 16dp - + 24dp 5dp 2dp - + 1dp 2dp 4dp 2dp - + 8dp 2dp - + 2dp 2dp 216dp @@ -258,17 +306,17 @@ 26dp 2dp - + 52dp - 24dp + 20dp 16dp 56dp 48dp - - 12dp + + 14dp - + 8dp 8dp 8dp @@ -286,11 +334,11 @@ 56dp 18dp - + 24dp 32dp - + 48dp 32dp 8dp @@ -302,18 +350,19 @@ 14sp 504dp - + 10dp - + 8dp @dimen/default_dialog_corner_radius 24dp - + 0dp + 0dp 0dp 44dp @@ -322,41 +371,42 @@ 16dp - + 1dp - + 0dp 0dp 0dp 0dp - 0dp 0dp + 0dp 0dp - 0dp 0dp - 0dp - 0dp - 0dp - 0dp - 0dp - 0dp - 0dp + 0dp + 0dp 0dp 0dp - 0dp - 0dp - 110dp - 200dp + 72dp + 16dp + 44dp + 216dp - - 22dp - 6dp + + 28dp + 6dp + 6dp - + 48dp 32dp 8dp + + 0dp + 32dp + 4dp + 16dp + 2dp diff --git a/res/values/id.xml b/res/values/id.xml index 508caffd48..af21b27caf 100644 --- a/res/values/id.xml +++ b/res/values/id.xml @@ -34,4 +34,8 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 868b5f39b8..847e4a8625 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -60,9 +60,9 @@ %1$s widget - Touch & hold the widget to move it around the Home screen + Touch & hold the widget to move it around the home screen - Add to Home screen + Add to home screen %1$s widget added to home screen @@ -108,7 +108,7 @@ Useful info at your fingertips - To get info without opening apps, you can add widgets to your Home screen + To get info without opening apps, you can add widgets to your home screen @@ -147,7 +147,7 @@ - No room on this Home screen + No room on this home screen No more room in the Favorites tray @@ -181,15 +181,15 @@ Allows an app to add shortcuts without user intervention. - read Home settings and shortcuts + read home settings and shortcuts Allows the app to read the settings and - shortcuts in Home. + shortcuts in home. - write Home settings and shortcuts + write home settings and shortcuts Allows the app to change the settings and - shortcuts in Home. + shortcuts in home. %1$s is not allowed to make phone calls @@ -217,7 +217,7 @@ Disabled %1$s - {count, plural, offset:1 + {count, plural, =1 {{app_name} has # notification} other {{app_name} has # notifications} } @@ -259,7 +259,7 @@ - Allow Home screen rotation + Allow home screen rotation When phone is rotated @@ -277,8 +277,11 @@ Show notification dots + + Developer Options + - Add app icons to Home screen + Add app icons to home screen For new apps @@ -303,6 +306,17 @@ %1$s waiting to install + + + App update required + + The app for this icon isn\'t updated. You can update manually to re-enable this shortcut, or remove the icon. + + Update + + Remove + @@ -312,7 +326,7 @@ - Add to Home screen + Add to home screen Move item here @@ -357,7 +371,7 @@ Folder created - Move to Home screen + Move to home screen Resize @@ -417,13 +431,17 @@ Got it - Turn off work apps + Pause work apps Turn on work apps Filter + + Search your phone + + Search your tablet Failed: %1$s diff --git a/res/values/styles.xml b/res/values/styles.xml index 818a032f94..21095109a6 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -76,7 +76,7 @@ @@ -203,6 +203,14 @@ no + + diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml index 299e92ea10..0f0dde24e2 100644 --- a/res/xml/backupscheme.xml +++ b/res/xml/backupscheme.xml @@ -2,6 +2,11 @@ + + + + + diff --git a/res/xml/default_test_workspace.xml b/res/xml/default_test_workspace.xml new file mode 100644 index 0000000000..bd718b3b14 --- /dev/null +++ b/res/xml/default_test_workspace.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml index 08698e7d9d..08025528ce 100644 --- a/res/xml/device_profiles.xml +++ b/res/xml/device_profiles.xml @@ -34,6 +34,8 @@ launcher:minHeightDps="300" launcher:iconImageSize="48" launcher:iconTextSize="13.0" + launcher:allAppsBorderSpace="16" + launcher:allAppsCellHeight="104" launcher:canBeDefault="true" /> @@ -63,6 +67,8 @@ launcher:minHeightDps="420" launcher:iconImageSize="48" launcher:iconTextSize="13.0" + launcher:allAppsBorderSpace="16" + launcher:allAppsCellHeight="104" launcher:canBeDefault="true" /> @@ -116,6 +130,8 @@ launcher:minHeightDps="694" launcher:iconImageSize="56" launcher:iconTextSize="14.4" + launcher:allAppsBorderSpace="16" + launcher:allAppsCellHeight="104" launcher:canBeDefault="true" /> @@ -140,10 +160,14 @@ launcher:name="6_by_5" launcher:numRows="5" launcher:numColumns="6" + launcher:numSearchContainerColumns="3" launcher:numFolderRows="3" launcher:numFolderColumns="3" launcher:numHotseatIcons="6" + launcher:hotseatColumnSpanLandscape="4" launcher:numAllAppsColumns="6" + launcher:isScalable="true" + launcher:devicePaddingId="@xml/paddings_6x5" launcher:dbFile="launcher_6_by_5.db" launcher:defaultLayoutId="@xml/default_workspace_6x5" launcher:deviceCategory="tablet" > @@ -152,14 +176,29 @@ launcher:name="Tablet" launcher:minWidthDps="900" launcher:minHeightDps="820" - launcher:minCellHeightDps="104" - launcher:minCellWidthDps="80" + launcher:minCellHeight="120" + launcher:minCellWidth="102" + launcher:minCellHeightLandscape="104" + launcher:minCellWidthLandscape="120" launcher:iconImageSize="60" launcher:iconTextSize="14" - launcher:borderSpaceDps="16" + launcher:borderSpaceHorizontal="16" + launcher:borderSpaceVertical="64" + launcher:borderSpaceLandscapeHorizontal="64" + launcher:borderSpaceLandscapeVertical="16" + launcher:horizontalMargin="54" + launcher:horizontalMarginLandscape="120" + launcher:allAppsCellWidth="96" + launcher:allAppsCellHeight="142" + launcher:allAppsCellWidthLandscape="126" + launcher:allAppsCellHeightLandscape="126" launcher:allAppsIconSize="60" launcher:allAppsIconTextSize="14" - launcher:allAppsCellSpacingDps="16" + launcher:allAppsBorderSpaceHorizontal="8" + launcher:allAppsBorderSpaceVertical="16" + launcher:allAppsBorderSpaceLandscape="16" + launcher:hotseatBorderSpace="58" + launcher:hotseatBorderSpaceLandscape="50.4" launcher:canBeDefault="true" /> diff --git a/res/xml/grayscale_icon_map.xml b/res/xml/grayscale_icon_map.xml new file mode 100644 index 0000000000..f6383ceb1e --- /dev/null +++ b/res/xml/grayscale_icon_map.xml @@ -0,0 +1,17 @@ + + + diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index 90de4987f5..8a0c909ed8 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -53,7 +53,7 @@ diff --git a/res/xml/paddings_6x5.xml b/res/xml/paddings_6x5.xml new file mode 100644 index 0000000000..a72f55412c --- /dev/null +++ b/res/xml/paddings_6x5.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index e3cfb59b60..21dbc5f7e0 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -65,7 +65,9 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_ICON_SURFACE, TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP, TYPE_WIDGETS_EDUCATION_DIALOG, - TYPE_TASKBAR_EDUCATION_DIALOG + TYPE_TASKBAR_EDUCATION_DIALOG, + TYPE_TASKBAR_ALL_APPS, + TYPE_OPTIONS_POPUP_DIALOG }) @Retention(RetentionPolicy.SOURCE) public @interface FloatingViewType {} @@ -85,23 +87,27 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_TASK_MENU = 1 << 11; public static final int TYPE_OPTIONS_POPUP = 1 << 12; public static final int TYPE_ICON_SURFACE = 1 << 13; + public static final int TYPE_OPTIONS_POPUP_DIALOG = 1 << 18; public static final int TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP = 1 << 14; public static final int TYPE_WIDGETS_EDUCATION_DIALOG = 1 << 15; public static final int TYPE_TASKBAR_EDUCATION_DIALOG = 1 << 16; + public static final int TYPE_TASKBAR_ALL_APPS = 1 << 17; + public static final int TYPE_ADD_TO_HOME_CONFIRMATION = 1 << 18; public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP | TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP - | TYPE_WIDGETS_EDUCATION_DIALOG | TYPE_TASKBAR_EDUCATION_DIALOG; + | TYPE_WIDGETS_EDUCATION_DIALOG | TYPE_TASKBAR_EDUCATION_DIALOG | TYPE_TASKBAR_ALL_APPS + | TYPE_OPTIONS_POPUP_DIALOG | TYPE_ADD_TO_HOME_CONFIRMATION; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE | TYPE_WIDGETS_EDUCATION_DIALOG - | TYPE_TASKBAR_EDUCATION_DIALOG; + | TYPE_TASKBAR_EDUCATION_DIALOG | TYPE_TASKBAR_ALL_APPS | TYPE_OPTIONS_POPUP_DIALOG; // Usually we show the back button when a floating view is open. Instead, hide for these types. public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE @@ -202,6 +208,13 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch return getView(activity, type, true /* mustBeOpen */); } + /** + * Returns whether there is at least one view of the given type where {@link #isOpen()} == true. + */ + public static boolean hasOpenView(ActivityContext activity, @FloatingViewType int type) { + return getOpenView(activity, type) != null; + } + /** * Returns a view matching FloatingViewType, and {@link #isOpen()} may be false (if animating * closed). diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index 300f22bd96..4b4a017c9d 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -383,7 +383,7 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O // Handle invalid resize across CellLayouts in the two panel UI. if (mCellLayout.getParent() instanceof Workspace) { - Workspace workspace = (Workspace) mCellLayout.getParent(); + Workspace workspace = (Workspace) mCellLayout.getParent(); CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout); if (pairedCellLayout != null) { Rect focusedCellLayoutBound = sTmpRect; @@ -570,7 +570,7 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); final CellLayout pairedCellLayout; if (mCellLayout.getParent() instanceof Workspace) { - Workspace workspace = (Workspace) mCellLayout.getParent(); + Workspace workspace = (Workspace) mCellLayout.getParent(); pairedCellLayout = workspace.getScreenPair(mCellLayout); } else { pairedCellLayout = null; diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 5b655a4740..64666b0041 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -27,9 +27,7 @@ import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build.VERSION; import android.os.Bundle; -import android.os.Process; import android.text.TextUtils; import android.util.ArrayMap; import android.util.AttributeSet; @@ -449,7 +447,7 @@ public class AutoInstallsLayout { // Auto installs should always support the current platform version. LauncherIcons li = LauncherIcons.obtain(mContext); mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap( - li.createBadgedIconBitmap(icon, Process.myUserHandle(), VERSION.SDK_INT).icon)); + li.createBadgedIconBitmap(icon).icon)); li.recycle(); mValues.put(Favorites.ICON_PACKAGE, mIconRes.getResourcePackageName(iconId)); diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index ec96c6de5d..73d3e3301c 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -25,30 +24,28 @@ import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; -import android.content.pm.LauncherApps; import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.UserHandle; -import android.util.Log; import androidx.annotation.IntDef; +import com.android.launcher3.DeviceProfile.DeviceProfileListenable; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.ViewCache; -import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.AppLauncher; import com.android.launcher3.views.ScrimView; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.util.ArrayList; +import java.util.List; /** * Launcher BaseActivity */ -public abstract class BaseActivity extends Activity implements ActivityContext { +public abstract class BaseActivity extends Activity implements AppLauncher, + DeviceProfileListenable { private static final String TAG = "BaseActivity"; @@ -142,6 +139,11 @@ public abstract class BaseActivity extends Activity implements ActivityContext { return mDeviceProfile; } + @Override + public List getOnDeviceProfileChangeListeners() { + return mDPChangeListeners; + } + /** * Returns {@link StatsLogManager} for user event logging. */ @@ -261,20 +263,6 @@ public abstract class BaseActivity extends Activity implements ActivityContext { protected void onActivityFlagsChanged(int changeBits) { } - public void addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) { - mDPChangeListeners.add(listener); - } - - public void removeOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) { - mDPChangeListeners.remove(listener); - } - - protected void dispatchDeviceProfileChanged() { - for (int i = mDPChangeListeners.size() - 1; i >= 0; i--) { - mDPChangeListeners.get(i).onDeviceProfileChanged(mDeviceProfile); - } - } - public void addMultiWindowModeChangedListener(MultiWindowModeChangedListener listener) { mMultiWindowModeChangedListeners.add(listener); } @@ -320,22 +308,6 @@ public abstract class BaseActivity extends Activity implements ActivityContext { writer.println(prefix + "mForceInvisible: " + mForceInvisible); } - /** - * A wrapper around the platform method with Launcher specific checks - */ - public void startShortcut(String packageName, String id, Rect sourceBounds, - Bundle startActivityOptions, UserHandle user) { - if (GO_DISABLE_WIDGETS) { - return; - } - try { - getSystemService(LauncherApps.class).startShortcut(packageName, id, sourceBounds, - startActivityOptions, user); - } catch (SecurityException | IllegalStateException e) { - Log.e(TAG, "Failed to start shortcut", e); - } - } - public static T fromContext(Context context) { if (context instanceof BaseActivity) { return (T) context; diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java index dd56ca3a23..6de3884b93 100644 --- a/src/com/android/launcher3/BaseDraggingActivity.java +++ b/src/com/android/launcher3/BaseDraggingActivity.java @@ -16,53 +16,33 @@ package com.android.launcher3; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import android.app.ActivityOptions; import android.app.WallpaperColors; import android.app.WallpaperManager; import android.app.WallpaperManager.OnColorsChangedListener; -import android.content.ActivityNotFoundException; import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; import android.content.res.Configuration; -import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Process; -import android.os.StrictMode; -import android.os.UserHandle; -import android.util.Log; import android.view.ActionMode; import android.view.Display; import android.view.View; -import android.view.WindowInsets.Type; -import android.view.WindowMetrics; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider; import com.android.launcher3.allapps.search.SearchAdapterProvider; -import com.android.launcher3.logging.InstanceId; -import com.android.launcher3.logging.InstanceIdSequence; -import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.touch.ItemClickHandler; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; import com.android.launcher3.util.DisplayController.Info; -import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.Themes; import com.android.launcher3.util.TraceHelper; @@ -165,108 +145,12 @@ public abstract class BaseDraggingActivity extends BaseActivity // no-op } + @Override @NonNull public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { - int left = 0, top = 0; - int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); - if (v instanceof BubbleTextView) { - // Launch from center of icon, not entire view - Drawable icon = ((BubbleTextView) v).getIcon(); - if (icon != null) { - Rect bounds = icon.getBounds(); - left = (width - bounds.width()) / 2; - top = v.getPaddingTop(); - width = bounds.width(); - height = bounds.height(); - } - } - ActivityOptions options = - ActivityOptions.makeClipRevealAnimation(v, left, top, width, height); - RunnableList callback = new RunnableList(); - addOnResumeCallback(callback::executeAllAndDestroy); - return new ActivityOptionsWrapper(options, callback); - } - - public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item) { - if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) { - Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); - return false; - } - - Bundle optsBundle = (v != null) ? getActivityLaunchOptions(v, item).toBundle() : null; - UserHandle user = item == null ? null : item.user; - - // Prepare intent - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (v != null) { - intent.setSourceBounds(Utilities.getViewBounds(v)); - } - try { - boolean isShortcut = (item instanceof WorkspaceItemInfo) - && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT - || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) - && !((WorkspaceItemInfo) item).isPromise(); - if (isShortcut) { - // Shortcuts need some special checks due to legacy reasons. - startShortcutIntentSafely(intent, optsBundle, item); - } else if (user == null || user.equals(Process.myUserHandle())) { - // Could be launching some bookkeeping activity - startActivity(intent, optsBundle); - } else { - getSystemService(LauncherApps.class).startMainActivity( - intent.getComponent(), user, intent.getSourceBounds(), optsBundle); - } - if (item != null) { - InstanceId instanceId = new InstanceIdSequence().newInstanceId(); - logAppLaunch(getStatsLogManager(), item, instanceId); - } - return true; - } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { - Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e); - } - return false; - } - - /** - * Creates and logs a new app launch event. - */ - public void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info, - InstanceId instanceId) { - statsLogManager.logger().withItemInfo(info).withInstanceId(instanceId) - .log(LAUNCHER_APP_LAUNCH_TAP); - } - - private void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info) { - try { - StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy(); - try { - // Temporarily disable deathPenalty on all default checks. For eg, shortcuts - // containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure - // is enabled by default on NYC. - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll() - .penaltyLog().build()); - - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - String id = ((WorkspaceItemInfo) info).getDeepShortcutId(); - String packageName = intent.getPackage(); - startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user); - } else { - // Could be launching some bookkeeping activity - startActivity(intent, optsBundle); - } - } finally { - StrictMode.setVmPolicy(oldPolicy); - } - } catch (SecurityException e) { - if (!onErrorStartingShortcut(intent, info)) { - throw e; - } - } - } - - protected boolean onErrorStartingShortcut(Intent intent, ItemInfo info) { - return false; + ActivityOptionsWrapper wrapper = super.getActivityLaunchOptions(v, item); + addOnResumeCallback(wrapper.onEndCallback::executeAllAndDestroy); + return wrapper; } @Override @@ -318,11 +202,7 @@ public abstract class BaseDraggingActivity extends BaseActivity protected WindowBounds getMultiWindowDisplaySize() { if (Utilities.ATLEAST_R) { - WindowMetrics wm = getWindowManager().getCurrentWindowMetrics(); - - Insets insets = wm.getWindowInsets().getInsets(Type.systemBars()); - return new WindowBounds(wm.getBounds(), - new Rect(insets.left, insets.top, insets.right, insets.bottom)); + return WindowBounds.fromWindowMetrics(getWindowManager().getCurrentWindowMetrics()); } // Note: Calls to getSize() can't rely on our cached DefaultDisplay since it can return // the app window size @@ -336,7 +216,14 @@ public abstract class BaseDraggingActivity extends BaseActivity * Creates and returns {@link SearchAdapterProvider} for build variant specific search result * views */ - public SearchAdapterProvider createSearchAdapterProvider(AllAppsContainerView allapps) { - return new DefaultSearchAdapterProvider(this, allapps); + @Override + public SearchAdapterProvider createSearchAdapterProvider( + ActivityAllAppsContainerView allApps) { + return new DefaultSearchAdapterProvider(this); + } + + @Override + public boolean isAppBlockedForSafeMode() { + return mIsSafeModeEnabled; } } diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 163b442b6e..5fb892554a 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -18,6 +18,8 @@ package com.android.launcher3; import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; +import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE; +import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import android.animation.Animator; @@ -49,11 +51,11 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.annotation.UiThread; -import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.accessibility.BaseAccessibilityDelegate; import com.android.launcher3.dot.DotInfo; +import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.graphics.IconShape; import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.icons.DotRenderer; @@ -64,12 +66,11 @@ import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.PackageItemInfo; -import com.android.launcher3.model.data.SearchActionItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.BubbleTextHolder; import com.android.launcher3.views.IconLabelDotView; import java.text.NumberFormat; @@ -95,7 +96,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private static final int MAX_SEARCH_LOOP_COUNT = 20; private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; - private static final float HIGHLIGHT_SCALE = 1.16f; private final PointF mTranslationForReorderBounce = new PointF(0, 0); private final PointF mTranslationForReorderPreview = new PointF(0, 0); @@ -145,11 +145,15 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private final boolean mIsRtl; private final int mIconSize; + @ViewDebug.ExportedProperty(category = "launcher") + private boolean mHideBadge = false; @ViewDebug.ExportedProperty(category = "launcher") private boolean mIsIconVisible = true; @ViewDebug.ExportedProperty(category = "launcher") private int mTextColor; @ViewDebug.ExportedProperty(category = "launcher") + private ColorStateList mTextColorStateList; + @ViewDebug.ExportedProperty(category = "launcher") private float mTextAlpha = 1; @ViewDebug.ExportedProperty(category = "launcher") @@ -170,7 +174,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private HandlerRunnable mIconLoadRequest; private boolean mEnableIconUpdateAnimation = false; - private BubbleTextHolder mBubbleTextHolder; public BubbleTextView(Context context) { this(context, null, 0); @@ -240,16 +243,27 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, super.onFocusChanged(focused, direction, previouslyFocusedRect); } + public void setHideBadge(boolean hideBadge) { + mHideBadge = hideBadge; + } + /** * Resets the view so it can be recycled. */ public void reset() { mDotInfo = null; - mDotParams.color = Color.TRANSPARENT; + mDotParams.dotColor = Color.TRANSPARENT; + mDotParams.appColor = Color.TRANSPARENT; cancelDotScaleAnim(); mDotParams.scale = 0f; mForceHideDot = false; setBackground(null); + + setTag(null); + if (mIconLoadRequest != null) { + mIconLoadRequest.cancel(); + mIconLoadRequest = null; + } } private void cancelDotScaleAnim() { @@ -295,7 +309,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @Override public void setAccessibilityDelegate(AccessibilityDelegate delegate) { - if (delegate instanceof LauncherAccessibilityDelegate) { + if (delegate instanceof BaseAccessibilityDelegate) { super.setAccessibilityDelegate(delegate); } else { // NO-OP @@ -347,25 +361,22 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, setDownloadStateContentDescription(info, info.getProgressLevel()); } - private void setItemInfo(ItemInfoWithIcon itemInfo) { + protected void setItemInfo(ItemInfoWithIcon itemInfo) { setTag(itemInfo); - if (mBubbleTextHolder != null) { - mBubbleTextHolder.onItemInfoUpdated(itemInfo); - } - } - - public void setBubbleTextHolder( - BubbleTextHolder bubbleTextHolder) { - mBubbleTextHolder = bubbleTextHolder; } @UiThread protected void applyIconAndLabel(ItemInfoWithIcon info) { boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER || mDisplay == DISPLAY_TASKBAR; - FastBitmapDrawable iconDrawable = info.newIcon(getContext(), useTheme); - mDotParams.color = IconPalette.getMutedColor(iconDrawable.getIconColor(), 0.54f); - + int flags = useTheme ? FLAG_THEMED : 0; + if (mHideBadge) { + flags |= FLAG_NO_BADGE; + } + FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags); + mDotParams.appColor = iconDrawable.getIconColor(); + mDotParams.dotColor = getContext().getResources() + .getColor(android.R.color.system_accent3_200, getContext().getTheme()); setIcon(iconDrawable); applyLabel(info); } @@ -631,12 +642,14 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @Override public void setTextColor(int color) { mTextColor = color; + mTextColorStateList = null; super.setTextColor(getModifiedColor()); } @Override public void setTextColor(ColorStateList colors) { mTextColor = colors.getDefaultColor(); + mTextColorStateList = colors; if (Float.compare(mTextAlpha, 1) == 0) { super.setTextColor(colors); } else { @@ -658,7 +671,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private void setTextAlpha(float alpha) { mTextAlpha = alpha; - super.setTextColor(getModifiedColor()); + if (mTextColorStateList != null) { + setTextColor(mTextColorStateList); + } else { + super.setTextColor(getModifiedColor()); + } } private int getModifiedColor() { @@ -847,6 +864,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } protected void applyCompoundDrawables(Drawable icon) { + if (icon == null) { + // Icon can be null when we use the BubbleTextView for text only. + return; + } + // If we had already set an icon before, disable relayout as the icon size is the // same as before. mDisableRelayout = mIcon != null; @@ -890,10 +912,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } else if (info instanceof WorkspaceItemInfo) { applyFromWorkspaceItem((WorkspaceItemInfo) info); mActivity.invalidateParent(info); - } else if (info instanceof PackageItemInfo) { - applyFromItemInfoWithIcon((PackageItemInfo) info); - } else if (info instanceof SearchActionItemInfo) { - applyFromItemInfoWithIcon((SearchActionItemInfo) info); + } else if (info != null) { + applyFromItemInfoWithIcon(info); } mDisableRelayout = false; @@ -997,19 +1017,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, getIconBounds(mIconSize, bounds); } - private int getIconSizeForDisplay(int display) { - DeviceProfile grid = mActivity.getDeviceProfile(); - switch (display) { - case DISPLAY_ALL_APPS: - return grid.allAppsIconSizePx; - case DISPLAY_FOLDER: - return grid.folderChildIconSizePx; - case DISPLAY_WORKSPACE: - default: - return grid.iconSizePx; - } - } - public void getSourceVisualDragBounds(Rect bounds) { getIconBounds(mIconSize, bounds); } @@ -1022,8 +1029,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } private void resetIconScale() { - if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).resetScale(); + if (mIcon != null) { + mIcon.resetScale(); } } @@ -1044,4 +1051,19 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, args.put("count", notificationCount); return icuCountFormat.format(args); } + + /** + * Starts a long press action and returns the corresponding pre-drag condition + */ + public PreDragCondition startLongPressAction() { + PopupContainerWithArrow popup = PopupContainerWithArrow.showForIcon(this); + return popup != null ? popup.createPreDragCondition(true) : null; + } + + /** + * Returns true if the view can show long-press popup + */ + public boolean canShowLongPressPopup() { + return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag()); + } } diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 38d5077411..3b24df2f18 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -49,6 +50,8 @@ public abstract class ButtonDropTarget extends TextView private static final int[] sTempCords = new int[2]; private static final int DRAG_VIEW_DROP_DURATION = 285; private static final float DRAG_VIEW_HOVER_OVER_OPACITY = 0.65f; + private static final int MAX_LINES_TEXT_MULTI_LINE = 2; + private static final int MAX_LINES_TEXT_SINGLE_LINE = 1; public static final int TOOLTIP_DEFAULT = 0; public static final int TOOLTIP_LEFT = 1; @@ -72,6 +75,8 @@ public abstract class ButtonDropTarget extends TextView protected CharSequence mText; protected Drawable mDrawable; private boolean mTextVisible = true; + private boolean mIconVisible = true; + private boolean mTextMultiLine = true; private PopupWindow mToolTip; private int mToolTipLocation; @@ -109,8 +114,7 @@ public abstract class ButtonDropTarget extends TextView // drawableLeft and drawableStart. mDrawable = getContext().getDrawable(resId).mutate(); mDrawable.setTintList(getTextColors()); - centerIcon(); - setCompoundDrawablesRelative(mDrawable, null, null, null); + updateIconVisibility(); } public void setDropTargetBar(DropTargetBar dropTargetBar) { @@ -140,7 +144,7 @@ public abstract class ButtonDropTarget extends TextView y = -getMeasuredHeight(); message.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); if (mToolTipLocation == TOOLTIP_LEFT) { - x = - getMeasuredWidth() - message.getMeasuredWidth() / 2; + x = -getMeasuredWidth() - message.getMeasuredWidth() / 2; } else { x = getMeasuredWidth() / 2 + message.getMeasuredWidth() / 2; } @@ -175,7 +179,12 @@ public abstract class ButtonDropTarget extends TextView @Override public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { - mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo); + if (options.isKeyboardDrag) { + mActive = false; + } else { + setupItemInfo(dragObject.dragInfo); + mActive = supportsDrop(dragObject.dragInfo); + } setVisibility(mActive ? View.VISIBLE : View.GONE); mAccessibleDrag = options.isAccessibleDrag; @@ -187,6 +196,11 @@ public abstract class ButtonDropTarget extends TextView return supportsDrop(dragObject.dragInfo); } + /** + * Setups button for the specified ItemInfo. + */ + protected abstract void setupItemInfo(ItemInfo info); + protected abstract boolean supportsDrop(ItemInfo info); public abstract boolean supportsAccessibilityDrop(ItemInfo info, View view); @@ -218,6 +232,7 @@ public abstract class ButtonDropTarget extends TextView final Rect to = getIconRect(d); final float scale = (float) to.width() / dragView.getMeasuredWidth(); dragView.detachContentView(/* reattachToPreviousParent= */ true); + mDropTargetBar.deferOnDragEnd(); Runnable onAnimationEndRunnable = () -> { @@ -305,13 +320,49 @@ public abstract class ButtonDropTarget extends TextView if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) { mTextVisible = isVisible; setText(newText); - centerIcon(); - setCompoundDrawablesRelative(mDrawable, null, null, null); - int drawablePadding = mTextVisible ? mDrawablePadding : 0; - setCompoundDrawablePadding(drawablePadding); + updateIconVisibility(); } } + /** + * Display button text over multiple lines when isMultiLine is true, single line otherwise. + */ + public void setTextMultiLine(boolean isMultiLine) { + if (mTextMultiLine != isMultiLine) { + mTextMultiLine = isMultiLine; + setSingleLine(!isMultiLine); + setMaxLines(isMultiLine ? MAX_LINES_TEXT_MULTI_LINE : MAX_LINES_TEXT_SINGLE_LINE); + int inputType = InputType.TYPE_CLASS_TEXT; + if (isMultiLine) { + inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; + + } + setInputType(inputType); + } + } + + protected boolean isTextMultiLine() { + return mTextMultiLine; + } + + /** + * Sets the button icon visible when isVisible is true, hides it otherwise. + */ + public void setIconVisible(boolean isVisible) { + if (mIconVisible != isVisible) { + mIconVisible = isVisible; + updateIconVisibility(); + } + } + + private void updateIconVisibility() { + if (mIconVisible) { + centerIcon(); + } + setCompoundDrawablesRelative(mIconVisible ? mDrawable : null, null, null, null); + setCompoundDrawablePadding(mIconVisible && mTextVisible ? mDrawablePadding : 0); + } + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index adb1613e9d..300e7bfb11 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -65,6 +65,7 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.folder.PreviewBackground; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.util.CellAndSpan; import com.android.launcher3.util.GridOccupancy; import com.android.launcher3.util.ParcelableSparseArray; @@ -93,7 +94,7 @@ public class CellLayout extends ViewGroup { private int mFixedCellWidth; private int mFixedCellHeight; @ViewDebug.ExportedProperty(category = "launcher") - private final Point mBorderSpace; + private Point mBorderSpace; @ViewDebug.ExportedProperty(category = "launcher") private int mCountX; @@ -148,7 +149,6 @@ public class CellLayout extends ViewGroup { private boolean mVisualizeDropLocation = true; private RectF mVisualizeGridRect = new RectF(); private Paint mVisualizeGridPaint = new Paint(); - private int mGridVisualizationPadding; private int mGridVisualizationRoundingRadius; private float mGridAlpha = 0f; private int mGridColor = 0; @@ -238,12 +238,7 @@ public class CellLayout extends ViewGroup { mActivity = ActivityContext.lookupContext(context); DeviceProfile deviceProfile = mActivity.getDeviceProfile(); - mBorderSpace = mContainerType == FOLDER - ? new Point(deviceProfile.folderCellLayoutBorderSpacePx) - : new Point(deviceProfile.cellLayoutBorderSpacePx); - - mCellWidth = mCellHeight = -1; - mFixedCellWidth = mFixedCellHeight = -1; + resetCellSizeInternal(deviceProfile); mCountX = deviceProfile.inv.numColumns; mCountY = deviceProfile.inv.numRows; @@ -265,8 +260,6 @@ public class CellLayout extends ViewGroup { mBackground.setAlpha(0); mGridColor = Themes.getAttrColor(getContext(), R.attr.workspaceAccentColor); - mGridVisualizationPadding = - res.getDimensionPixelSize(R.dimen.grid_visualization_cell_spacing); mGridVisualizationRoundingRadius = res.getDimensionPixelSize(R.dimen.grid_visualization_rounding_radius); mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx); @@ -292,7 +285,7 @@ public class CellLayout extends ViewGroup { for (int i = 0; i < mDragOutlineAnims.length; i++) { final InterruptibleInOutAnimator anim = - new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue); + new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue); anim.getAnimator().setInterpolator(mEaseOutInterpolator); final int thisIndex = i; anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { @@ -366,6 +359,12 @@ public class CellLayout extends ViewGroup { return mShortcutsAndWidgets.getLayerType() == LAYER_TYPE_HARDWARE; } + /** + * Change sizes of cells + * + * @param width the new width of the cells + * @param height the new height of the cells + */ public void setCellDimensions(int width, int height) { mFixedCellWidth = mCellWidth = width; mFixedCellHeight = mCellHeight = height; @@ -373,6 +372,33 @@ public class CellLayout extends ViewGroup { mBorderSpace); } + private void resetCellSizeInternal(DeviceProfile deviceProfile) { + switch (mContainerType) { + case FOLDER: + mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx); + break; + case HOTSEAT: + mBorderSpace = new Point(deviceProfile.hotseatBorderSpace, + deviceProfile.hotseatBorderSpace); + break; + case WORKSPACE: + default: + mBorderSpace = new Point(deviceProfile.cellLayoutBorderSpacePx); + break; + } + + mCellWidth = mCellHeight = -1; + mFixedCellWidth = mFixedCellHeight = -1; + } + + /** + * Reset the cell sizes and border space + */ + public void resetCellSize(DeviceProfile deviceProfile) { + resetCellSizeInternal(deviceProfile); + requestLayout(); + } + public void setGridSize(int x, int y) { mCountX = x; mCountY = y; @@ -563,8 +589,8 @@ public class CellLayout extends ViewGroup { protected void visualizeGrid(Canvas canvas) { DeviceProfile dp = mActivity.getDeviceProfile(); - int paddingX = (int) Math.min((mCellWidth - dp.iconSizePx) / 2, mGridVisualizationPadding); - int paddingY = (int) Math.min((mCellHeight - dp.iconSizePx) / 2, mGridVisualizationPadding); + int paddingX = Math.min((mCellWidth - dp.iconSizePx) / 2, dp.gridVisualizationPaddingX); + int paddingY = Math.min((mCellHeight - dp.iconSizePx) / 2, dp.gridVisualizationPaddingY); mVisualizeGridRect.set(paddingX, paddingY, mCellWidth - paddingX, mCellHeight - paddingY); @@ -1159,9 +1185,7 @@ public class CellLayout extends ViewGroup { // Apply local extracted color if the DragView is an AppWidgetHostViewDrawable. View view = dragObject.dragView.getContentView(); if (view instanceof LauncherAppWidgetHostView) { - Launcher launcher = Launcher.getLauncher(getContext()); - Workspace workspace = launcher.getWorkspace(); - int screenId = workspace.getIdForScreen(this); + int screenId = getWorkspace().getIdForScreen(this); cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect); ((LauncherAppWidgetHostView) view).handleDrag(mTempRect, this, screenId); @@ -1174,11 +1198,24 @@ public class CellLayout extends ViewGroup { return getContext().getString(R.string.move_to_hotseat_position, Math.max(cellX, cellY) + 1); } else { - return getContext().getString(R.string.move_to_empty_cell, - cellY + 1, cellX + 1); + Workspace workspace = getWorkspace(); + int row = cellY + 1; + int col = workspace.mIsRtl ? mCountX - cellX : cellX + 1; + int panelCount = workspace.getPanelCount(); + if (panelCount > 1) { + // Increment the column if the target is on the right side of a two panel home + int screenId = workspace.getIdForScreen(this); + int pageIndex = workspace.getPageIndexForScreenId(screenId); + col += (pageIndex % panelCount) * mCountX; + } + return getContext().getString(R.string.move_to_empty_cell, row, col); } } + private Workspace getWorkspace() { + return Launcher.cast(mActivity).getWorkspace(); + } + public void clearDragOutlines() { final int oldIndex = mDragOutlineCurrent; mDragOutlineAnims[oldIndex].animateOut(); @@ -2233,7 +2270,7 @@ public class CellLayout extends ViewGroup { private void commitTempPlacement(View dragView) { mTmpOccupied.copyTo(mOccupied); - int screenId = Launcher.cast(mActivity).getWorkspace().getIdForScreen(this); + int screenId = getWorkspace().getIdForScreen(this); int container = Favorites.CONTAINER_DESKTOP; if (mContainerType == HOTSEAT) { @@ -2398,7 +2435,7 @@ public class CellLayout extends ViewGroup { // First we determine if things have moved enough to cause a different layout ItemConfiguration swapSolution = findReorderSolution(pixelXY[0], pixelXY[1], spanX, spanY, - spanX, spanY, direction, dragView, true, new ItemConfiguration()); + spanX, spanY, direction, dragView, true, new ItemConfiguration()); setUseTempCoords(true); if (swapSolution != null && swapSolution.isSolution) { @@ -2435,7 +2472,7 @@ public class CellLayout extends ViewGroup { // direction vector, since we want the solution to match the preview, and it's possible // that the exact position of the item has changed to result in a new reordering outcome. if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP) - && mPreviousReorderDirection[0] != INVALID_DIRECTION) { + && mPreviousReorderDirection[0] != INVALID_DIRECTION) { mDirectionVector[0] = mPreviousReorderDirection[0]; mDirectionVector[1] = mPreviousReorderDirection[1]; // We reset this vector after drop @@ -2451,7 +2488,7 @@ public class CellLayout extends ViewGroup { // Find a solution involving pushing / displacing any items in the way ItemConfiguration swapSolution = findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, - spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration()); + spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration()); // We attempt the approach which doesn't shuffle views at all ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX, @@ -2691,12 +2728,24 @@ public class CellLayout extends ViewGroup { } public void markCellsAsOccupiedForView(View view) { + if (view instanceof LauncherAppWidgetHostView + && view.getTag() instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag(); + mOccupied.markCells(info.cellX, info.cellY, info.spanX, info.spanY, true); + return; + } if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, true); } public void markCellsAsUnoccupiedForView(View view) { + if (view instanceof LauncherAppWidgetHostView + && view.getTag() instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag(); + mOccupied.markCells(info.cellX, info.cellY, info.spanX, info.spanY, false); + return; + } if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false); diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index af85594779..4daca8b109 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -7,15 +7,19 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Bundle; +import android.os.Process; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; @@ -23,6 +27,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.net.URISyntaxException; +import java.util.Collections; import java.util.List; /** @@ -43,6 +48,8 @@ public class DefaultLayoutParser extends AutoInstallsLayout { private static final String ATTR_CONTAINER = "container"; private static final String ATTR_SCREEN = "screen"; private static final String ATTR_FOLDER_ITEMS = "folderItems"; + private static final String ATTR_SHORTCUT_ID = "shortcutId"; + private static final String ATTR_PACKAGE_NAME = "packageName"; // TODO: Remove support for this broadcast, instead use widget options to send bind time options private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = @@ -178,7 +185,6 @@ public class DefaultLayoutParser extends AutoInstallsLayout { } } - /** * Shortcut parser which allows any uri and not just web urls. */ @@ -188,6 +194,35 @@ public class DefaultLayoutParser extends AutoInstallsLayout { super(iconRes); } + @Override + public int parseAndAdd(XmlPullParser parser) { + final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String shortcutId = getAttributeValue(parser, ATTR_SHORTCUT_ID); + if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(shortcutId)) { + return parseAndAddDeepShortcut(shortcutId, packageName); + } + return super.parseAndAdd(parser); + } + + /** + * This method parses and adds a deep shortcut. + * @return item id if the shortcut is successfully added else -1 + */ + private int parseAndAddDeepShortcut(String shortcutId, String packageName) { + try { + LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); + launcherApps.pinShortcuts(packageName, Collections.singletonList(shortcutId), + Process.myUserHandle()); + Intent intent = ShortcutKey.makeIntent(shortcutId, packageName); + mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON); + return addShortcut(null, intent, Favorites.ITEM_TYPE_DEEP_SHORTCUT); + } catch (Exception e) { + Log.e(TAG, "Unable to pin the shortcut for shortcut id = " + shortcutId + + " and package name = " + packageName); + } + return -1; + } + @Override protected Intent parseIntent(XmlPullParser parser) { String uri = null; diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 477964a6f4..95d3ad9dbb 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -84,6 +84,9 @@ public class DeleteDropTarget extends ButtonDropTarget { return LauncherAccessibilityDelegate.REMOVE; } + @Override + protected void setupItemInfo(ItemInfo info) {} + @Override protected boolean supportsDrop(ItemInfo info) { return true; @@ -160,7 +163,7 @@ public class DeleteDropTarget extends ButtonDropTarget { // Remove the item from launcher and the db, we can ignore the containerInfo in this call // because we already remove the drag view from the folder (if the drag originated from // a folder) in Folder.beginDrag() - mLauncher.removeItem(view, item, true /* deleteFromDb */); + mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop"); mLauncher.getWorkspace().stripEmptyScreens(); mLauncher.getDragLayer() .announceForAccessibility(getContext().getString(R.string.item_removed)); diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 6dfb6c81ee..b2763977ca 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -16,6 +16,10 @@ package com.android.launcher3; +import static com.android.launcher3.InvariantDeviceProfile.INDEX_DEFAULT; +import static com.android.launcher3.InvariantDeviceProfile.INDEX_LANDSCAPE; +import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_LANDSCAPE; +import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT; import static com.android.launcher3.ResourceUtils.pxFromDp; import static com.android.launcher3.Utilities.dpiFromPx; import static com.android.launcher3.Utilities.pxFromSp; @@ -34,7 +38,6 @@ import android.view.Surface; import com.android.launcher3.CellLayout.ContainerType; import com.android.launcher3.DevicePaddings.DevicePadding; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.icons.IconNormalizer; @@ -44,13 +47,14 @@ import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.WindowBounds; import java.io.PrintWriter; +import java.util.List; @SuppressLint("NewApi") public class DeviceProfile { private static final int DEFAULT_DOT_SIZE = 100; // Ratio of empty space, qsb should take up to appear visually centered. - private static final float QSB_CENTER_FACTOR = .325f; + private final float mQsbCenterFactor; public final InvariantDeviceProfile inv; private final Info mInfo; @@ -61,10 +65,12 @@ public class DeviceProfile { public final boolean isPhone; public final boolean transposeLayoutWithOrientation; public final boolean isTwoPanels; + public final boolean isQsbInline; // Device properties in current orientation public final boolean isLandscape; public final boolean isMultiWindowMode; + public final boolean isGestureMode; public final int windowX; public final int windowY; @@ -72,6 +78,7 @@ public class DeviceProfile { public final int heightPx; public final int availableWidthPx; public final int availableHeightPx; + public final int rotationHint; public final float aspectRatio; @@ -90,19 +97,20 @@ public class DeviceProfile { private static final float TALL_DEVICE_EXTRA_SPACE_THRESHOLD_DP = 252; private static final float TALL_DEVICE_MORE_EXTRA_SPACE_THRESHOLD_DP = 268; - // To evenly space the icons, increase the left/right margins for tablets in portrait mode. - private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4; - // Workspace public final int desiredWorkspaceHorizontalMarginOriginalPx; public int desiredWorkspaceHorizontalMarginPx; + public int gridVisualizationPaddingX; + public int gridVisualizationPaddingY; public Point cellLayoutBorderSpaceOriginalPx; public Point cellLayoutBorderSpacePx; - public final int cellLayoutPaddingLeftRightPx; - public final int cellLayoutBottomPaddingPx; + public Rect cellLayoutPaddingPx = new Rect(); + public final int edgeMarginPx; - public float workspaceSpringLoadShrinkFactor; + public float workspaceSpringLoadShrunkTop; + public float workspaceSpringLoadShrunkBottom; public final int workspaceSpringLoadedBottomSpace; + public final int workspaceSpringLoadedMinNextPageVisiblePx; private final int extraSpace; public int workspaceTopPadding; @@ -153,40 +161,45 @@ public class DeviceProfile { public final int numShownHotseatIcons; public int hotseatCellHeightPx; private final int hotseatExtraVerticalSize; + private final boolean areNavButtonsInline; // In portrait: size = height, in landscape: size = width public int hotseatBarSizePx; public int hotseatBarTopPaddingPx; public final int hotseatBarBottomPaddingPx; + public int springLoadedHotseatBarTopMarginPx; // Start is the side next to the nav bar, end is the side next to the workspace public final int hotseatBarSidePaddingStartPx; public final int hotseatBarSidePaddingEndPx; public final int hotseatQsbHeight; + public int hotseatBorderSpace; public final float qsbBottomMarginOriginalPx; public int qsbBottomMarginPx; + public int qsbWidth; // only used when isQsbInline // All apps - public Point allAppsCellSpacePx; - public int allAppsOpenVerticalTranslate; + public Point allAppsBorderSpacePx; + public int allAppsShiftRange; + public int allAppsTopPadding; + public int bottomSheetTopPadding; public int allAppsCellHeightPx; public int allAppsCellWidthPx; public int allAppsIconSizePx; public int allAppsIconDrawablePaddingPx; public int allAppsLeftRightPadding; + public int allAppsLeftRightMargin; public final int numShownAllAppsColumns; public float allAppsIconTextSizePx; // Overview - public final boolean overviewShowAsGrid; public int overviewTaskMarginPx; public int overviewTaskMarginGridPx; public int overviewTaskIconSizePx; public int overviewTaskIconDrawableSizePx; public int overviewTaskIconDrawableSizeGridPx; public int overviewTaskThumbnailTopMarginPx; - public final int overviewActionsMarginThreeButtonPx; - public final int overviewActionsTopMarginGesturePx; - public final int overviewActionsBottomMarginGesturePx; + public final int overviewActionsHeight; + public final int overviewActionsTopMarginPx; public final int overviewActionsButtonSpacing; public int overviewPageSpacing; public int overviewRowSpacing; @@ -197,8 +210,14 @@ public class DeviceProfile { // Drop Target public int dropTargetBarSizePx; + public int dropTargetBarTopMarginPx; + public int dropTargetBarBottomMarginPx; public int dropTargetDragPaddingPx; public int dropTargetTextSizePx; + public int dropTargetHorizontalPaddingPx; + public int dropTargetVerticalPaddingPx; + public int dropTargetGapPx; + public int dropTargetButtonWorkspaceEdgeGapPx; // Insets private final Rect mInsets = new Rect(); @@ -216,6 +235,7 @@ public class DeviceProfile { // Whether Taskbar will inset the bottom of apps by taskbarSize. public boolean isTaskbarPresentInApps; public int taskbarSize; + public int stashedTaskbarSize; // DragController public int flingToDeleteThresholdVelocity; @@ -223,98 +243,94 @@ public class DeviceProfile { /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */ DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds, boolean isMultiWindowMode, boolean transposeLayoutWithOrientation, - boolean useTwoPanels) { + boolean useTwoPanels, boolean isGestureMode) { this.inv = inv; this.isLandscape = windowBounds.isLandscape(); this.isMultiWindowMode = isMultiWindowMode; this.transposeLayoutWithOrientation = transposeLayoutWithOrientation; + this.isGestureMode = isGestureMode; windowX = windowBounds.bounds.left; windowY = windowBounds.bounds.top; + this.rotationHint = windowBounds.rotationHint; + mInsets.set(windowBounds.insets); isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode; + // Determine device posture. + mInfo = info; + isTablet = info.isTablet(windowBounds); + isPhone = !isTablet; + isTwoPanels = isTablet && useTwoPanels; + isTaskbarPresent = isTablet && ApiWrapper.TASKBAR_DRAWN_IN_PROCESS; + + // Some more constants. + context = getContext(context, info, isVerticalBarLayout() || (isTablet && isLandscape) + ? Configuration.ORIENTATION_LANDSCAPE + : Configuration.ORIENTATION_PORTRAIT, + windowBounds); + final Resources res = context.getResources(); + mMetrics = res.getDisplayMetrics(); // Determine sizes. widthPx = windowBounds.bounds.width(); heightPx = windowBounds.bounds.height(); availableWidthPx = windowBounds.availableSize.x; - availableHeightPx = windowBounds.availableSize.y; - - mInfo = info; - isTablet = info.isTablet(windowBounds); - isPhone = !isTablet; - isTwoPanels = isTablet && useTwoPanels; + availableHeightPx = windowBounds.availableSize.y; aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx); boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0; - - // Some more constants - context = getContext(context, info, isVerticalBarLayout() - ? Configuration.ORIENTATION_LANDSCAPE - : Configuration.ORIENTATION_PORTRAIT); - mMetrics = context.getResources().getDisplayMetrics(); - final Resources res = context.getResources(); + mQsbCenterFactor = res.getFloat(R.dimen.qsb_center_factor); if (isTwoPanels) { if (isLandscape) { - mTypeIndex = InvariantDeviceProfile.INDEX_TWO_PANEL_LANDSCAPE; + mTypeIndex = INDEX_TWO_PANEL_LANDSCAPE; } else { - mTypeIndex = InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT; + mTypeIndex = INDEX_TWO_PANEL_PORTRAIT; } } else { if (isLandscape) { - mTypeIndex = InvariantDeviceProfile.INDEX_LANDSCAPE; + mTypeIndex = INDEX_LANDSCAPE; } else { - mTypeIndex = InvariantDeviceProfile.INDEX_DEFAULT; + mTypeIndex = INDEX_DEFAULT; } } - hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height); - isTaskbarPresent = isTablet && ApiWrapper.TASKBAR_DRAWN_IN_PROCESS - && FeatureFlags.ENABLE_TASKBAR.get(); if (isTaskbarPresent) { taskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_size); + stashedTaskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size); } edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); desiredWorkspaceHorizontalMarginPx = getHorizontalMarginPx(inv, res); desiredWorkspaceHorizontalMarginOriginalPx = desiredWorkspaceHorizontalMarginPx; + gridVisualizationPaddingX = res.getDimensionPixelSize( + R.dimen.grid_visualization_horizontal_cell_spacing); + gridVisualizationPaddingY = res.getDimensionPixelSize( + R.dimen.grid_visualization_vertical_cell_spacing); - allAppsOpenVerticalTranslate = res.getDimensionPixelSize( - R.dimen.all_apps_open_vertical_translate); + bottomSheetTopPadding = mInsets.top // statusbar height + + res.getDimensionPixelSize(R.dimen.bottom_sheet_extra_top_padding) + + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding + allAppsTopPadding = isTablet ? bottomSheetTopPadding : 0; + allAppsShiftRange = isTablet + ? heightPx - allAppsTopPadding + : res.getDimensionPixelSize(R.dimen.all_apps_starting_vertical_translate); folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale); folderContentPaddingLeftRight = res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right); folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top); cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv); - allAppsCellSpacePx = new Point( - pxFromDp(inv.borderSpaces[InvariantDeviceProfile.INDEX_ALL_APPS].x, mMetrics, 1f), - pxFromDp(inv.borderSpaces[InvariantDeviceProfile.INDEX_ALL_APPS].y, mMetrics, 1f)); + allAppsBorderSpacePx = new Point( + pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics), + pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics)); cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx); - folderCellLayoutBorderSpaceOriginalPx = pxFromDp(inv.folderBorderSpace, mMetrics, 1f); + folderCellLayoutBorderSpaceOriginalPx = pxFromDp(inv.folderBorderSpace, mMetrics); folderCellLayoutBorderSpacePx = new Point(folderCellLayoutBorderSpaceOriginalPx, folderCellLayoutBorderSpaceOriginalPx); - int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet - ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1; - int cellLayoutPadding = isScalableGrid - ? 0 - : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding); - - if (isTwoPanels) { - cellLayoutPaddingLeftRightPx = 0; - cellLayoutBottomPaddingPx = 0; - } else if (isLandscape) { - cellLayoutPaddingLeftRightPx = 0; - cellLayoutBottomPaddingPx = cellLayoutPadding; - } else { - cellLayoutPaddingLeftRightPx = cellLayoutPaddingLeftRightMultiplier * cellLayoutPadding; - cellLayoutBottomPaddingPx = 0; - } - workspacePageIndicatorHeight = res.getDimensionPixelSize( R.dimen.workspace_page_indicator_height); mWorkspacePageIndicatorOverlapWorkspace = @@ -324,41 +340,73 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size); + dropTargetBarTopMarginPx = res.getDimensionPixelSize(R.dimen.drop_target_top_margin); + dropTargetBarBottomMarginPx = res.getDimensionPixelSize(R.dimen.drop_target_bottom_margin); dropTargetDragPaddingPx = res.getDimensionPixelSize(R.dimen.drop_target_drag_padding); dropTargetTextSizePx = res.getDimensionPixelSize(R.dimen.drop_target_text_size); + dropTargetHorizontalPaddingPx = res.getDimensionPixelSize( + R.dimen.drop_target_button_drawable_horizontal_padding); + dropTargetVerticalPaddingPx = res.getDimensionPixelSize( + R.dimen.drop_target_button_drawable_vertical_padding); + dropTargetGapPx = res.getDimensionPixelSize(R.dimen.drop_target_button_gap); + dropTargetButtonWorkspaceEdgeGapPx = res.getDimensionPixelSize( + R.dimen.drop_target_button_workspace_edge_gap); workspaceSpringLoadedBottomSpace = res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space); + workspaceSpringLoadedMinNextPageVisiblePx = res.getDimensionPixelSize( + R.dimen.dynamic_grid_spring_loaded_min_next_space_visible); workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x); - numShownHotseatIcons = - isTwoPanels ? inv.numDatabaseHotseatIcons : inv.numShownHotseatIcons; + hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height); + // Whether QSB might be inline in appropriate orientation (e.g. landscape). + boolean canQsbInline = (isTwoPanels ? inv.inlineQsb[INDEX_TWO_PANEL_PORTRAIT] + || inv.inlineQsb[INDEX_TWO_PANEL_LANDSCAPE] + : inv.inlineQsb[INDEX_DEFAULT] || inv.inlineQsb[INDEX_LANDSCAPE]) + && hotseatQsbHeight > 0; + isQsbInline = inv.inlineQsb[mTypeIndex] && canQsbInline; + + // We shrink hotseat sizes regardless of orientation, if nav buttons are inline and QSB + // might be inline in either orientations, to keep hotseat size consistent across rotation. + areNavButtonsInline = isTaskbarPresent && !isGestureMode; + if (areNavButtonsInline && canQsbInline) { + numShownHotseatIcons = inv.numShrunkenHotseatIcons; + } else { + numShownHotseatIcons = + isTwoPanels ? inv.numDatabaseHotseatIcons : inv.numShownHotseatIcons; + } + numShownAllAppsColumns = isTwoPanels ? inv.numDatabaseAllAppsColumns : inv.numAllAppsColumns; hotseatBarSizeExtraSpacePx = 0; hotseatBarTopPaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding); - hotseatBarBottomPaddingPx = (isTallDevice ? 0 - : res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding)) - + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding); + if (isQsbInline) { + hotseatBarBottomPaddingPx = res.getDimensionPixelSize(R.dimen.inline_qsb_bottom_margin); + } else { + hotseatBarBottomPaddingPx = (isTallDevice ? res.getDimensionPixelSize( + R.dimen.dynamic_grid_hotseat_bottom_tall_padding) + : res.getDimensionPixelSize( + R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding)) + + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding); + } + + springLoadedHotseatBarTopMarginPx = res.getDimensionPixelSize( + R.dimen.spring_loaded_hotseat_top_margin); hotseatBarSidePaddingEndPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding); // Add a bit of space between nav bar and hotseat in vertical bar layout. hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0; hotseatExtraVerticalSize = res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size); - updateHotseatIconSize( - pxFromDp(inv.iconSize[InvariantDeviceProfile.INDEX_DEFAULT], mMetrics, 1f)); + updateHotseatIconSize(pxFromDp(inv.iconSize[INDEX_DEFAULT], mMetrics)); qsbBottomMarginOriginalPx = isScalableGrid ? res.getDimensionPixelSize(R.dimen.scalable_grid_qsb_bottom_margin) : 0; - overviewShowAsGrid = isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get(); - overviewTaskMarginPx = overviewShowAsGrid - ? res.getDimensionPixelSize(R.dimen.overview_task_margin_focused) - : res.getDimensionPixelSize(R.dimen.overview_task_margin); + overviewTaskMarginPx = res.getDimensionPixelSize(R.dimen.overview_task_margin); overviewTaskMarginGridPx = res.getDimensionPixelSize(R.dimen.overview_task_margin_grid); overviewTaskIconSizePx = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_size); overviewTaskIconDrawableSizePx = @@ -366,34 +414,14 @@ public class DeviceProfile { overviewTaskIconDrawableSizeGridPx = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid); overviewTaskThumbnailTopMarginPx = overviewTaskIconSizePx + overviewTaskMarginPx * 2; - if (overviewShowAsGrid) { - if (isLandscape) { - overviewActionsTopMarginGesturePx = res.getDimensionPixelSize( - R.dimen.overview_actions_top_margin_gesture_grid_landscape); - overviewActionsBottomMarginGesturePx = res.getDimensionPixelSize( - R.dimen.overview_actions_bottom_margin_gesture_grid_landscape); - overviewPageSpacing = res.getDimensionPixelSize( - R.dimen.overview_page_spacing_grid_landscape); - } else { - overviewActionsTopMarginGesturePx = res.getDimensionPixelSize( - R.dimen.overview_actions_top_margin_gesture_grid_portrait); - overviewActionsBottomMarginGesturePx = res.getDimensionPixelSize( - R.dimen.overview_actions_bottom_margin_gesture_grid_portrait); - overviewPageSpacing = res.getDimensionPixelSize( - R.dimen.overview_page_spacing_grid_portrait); - } - overviewActionsButtonSpacing = res.getDimensionPixelSize( - R.dimen.overview_actions_button_spacing_grid); - } else { - overviewActionsTopMarginGesturePx = res.getDimensionPixelSize( - R.dimen.overview_actions_margin_gesture); - overviewActionsBottomMarginGesturePx = overviewActionsTopMarginGesturePx; - overviewPageSpacing = res.getDimensionPixelSize(R.dimen.overview_page_spacing); - overviewActionsButtonSpacing = res.getDimensionPixelSize( - R.dimen.overview_actions_button_spacing); - } - overviewActionsMarginThreeButtonPx = res.getDimensionPixelSize( - R.dimen.overview_actions_margin_three_button); + // In vertical bar, use the smaller task margin for the top regardless of mode. + overviewActionsTopMarginPx = isVerticalBarLayout() + ? overviewTaskMarginPx + : res.getDimensionPixelSize(R.dimen.overview_actions_top_margin); + overviewPageSpacing = res.getDimensionPixelSize(R.dimen.overview_page_spacing); + overviewActionsButtonSpacing = res.getDimensionPixelSize( + R.dimen.overview_actions_button_spacing); + overviewActionsHeight = res.getDimensionPixelSize(R.dimen.overview_actions_height); // Grid task's top margin is only overviewTaskIconSizePx + overviewTaskMarginGridPx, but // overviewTaskThumbnailTopMarginPx is applied to all TaskThumbnailView, so exclude the // extra margin when calculating row spacing. @@ -401,9 +429,7 @@ public class DeviceProfile { - overviewTaskMarginGridPx; overviewRowSpacing = res.getDimensionPixelSize(R.dimen.overview_grid_row_spacing) - extraTopMargin; - overviewGridSideMargin = isLandscape - ? res.getDimensionPixelSize(R.dimen.overview_grid_side_margin_landscape) - : res.getDimensionPixelSize(R.dimen.overview_grid_side_margin_portrait); + overviewGridSideMargin = res.getDimensionPixelSize(R.dimen.overview_grid_side_margin); // Calculate all of the remaining variables. extraSpace = updateAvailableDimensions(res); @@ -457,8 +483,18 @@ public class DeviceProfile { // Recalculate the available dimensions using the new hotseat size. updateAvailableDimensions(res); } + + int cellLayoutPadding = + isTwoPanels ? cellLayoutBorderSpacePx.x / 2 : res.getDimensionPixelSize( + R.dimen.cell_layout_padding); + cellLayoutPaddingPx = new Rect(cellLayoutPadding, cellLayoutPadding, cellLayoutPadding, + cellLayoutPadding); updateWorkspacePadding(); + // Hotseat and QSB width depends on updated cellSize and workspace padding + hotseatBorderSpace = calculateHotseatBorderSpace(); + qsbWidth = calculateQsbWidth(); + flingToDeleteThresholdVelocity = res.getDimensionPixelSize( R.dimen.drag_flingToDeleteMinVelocity); @@ -469,6 +505,28 @@ public class DeviceProfile { new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE); } + /** + * QSB width is always calculated because when in 3 button nav the width doesn't follow the + * width of the hotseat. + */ + private int calculateQsbWidth() { + if (isQsbInline) { + int columns = getPanelCount() * inv.numColumns; + return getIconToIconWidthForColumns(columns) + - iconSizePx * numShownHotseatIcons + - hotseatBorderSpace * numShownHotseatIcons; + } else { + int columns = inv.hotseatColumnSpan[mTypeIndex]; + return getIconToIconWidthForColumns(columns); + } + } + + private int getIconToIconWidthForColumns(int columns) { + return columns * getCellSize().x + + (columns - 1) * cellLayoutBorderSpacePx.x + - (getCellSize().x - iconSizePx); // left and right cell space + } + private int getHorizontalMarginPx(InvariantDeviceProfile idp, Resources res) { if (isVerticalBarLayout()) { return 0; @@ -493,21 +551,21 @@ public class DeviceProfile { } private Point getCellLayoutBorderSpace(InvariantDeviceProfile idp) { + return getCellLayoutBorderSpace(idp, 1f); + + } + + private Point getCellLayoutBorderSpace(InvariantDeviceProfile idp, float scale) { if (!isScalableGrid) { return new Point(0, 0); } - int horizontalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].x, mMetrics); - int verticalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].y, mMetrics); + int horizontalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].x, mMetrics, scale); + int verticalSpacePx = pxFromDp(idp.borderSpaces[mTypeIndex].y, mMetrics, scale); return new Point(horizontalSpacePx, verticalSpacePx); } - private Point getCellLayoutBorderSpaceScaled(InvariantDeviceProfile idp, float scale) { - Point original = getCellLayoutBorderSpace(idp); - return new Point((int) (original.x * scale), (int) (original.y * scale)); - } - public Info getDisplayInfo() { return mInfo; } @@ -529,13 +587,15 @@ public class DeviceProfile { } public Builder toBuilder(Context context) { - WindowBounds bounds = - new WindowBounds(widthPx, heightPx, availableWidthPx, availableHeightPx); + WindowBounds bounds = new WindowBounds( + widthPx, heightPx, availableWidthPx, availableHeightPx, rotationHint); bounds.bounds.offsetTo(windowX, windowY); + bounds.insets.set(mInsets); return new Builder(context, inv, mInfo) .setWindowBounds(bounds) .setUseTwoPanels(isTwoPanels) - .setMultiWindowMode(isMultiWindowMode); + .setMultiWindowMode(isMultiWindowMode) + .setGestureMode(isGestureMode); } public DeviceProfile copy(Context context) { @@ -558,7 +618,6 @@ public class DeviceProfile { float appWidgetScaleX = (float) profile.getCellSize().x / getCellSize().x; float appWidgetScaleY = (float) profile.getCellSize().y / getCellSize().y; profile.appWidgetScale.set(appWidgetScaleX, appWidgetScaleY); - profile.updateWorkspacePadding(); return profile; } @@ -592,14 +651,20 @@ public class DeviceProfile { + textHeight + (topBottomPadding * 2); } - private void updateAllAppsWidth() { - if (isTwoPanels) { + private void updateAllAppsContainerWidth(Resources res) { + int cellLayoutHorizontalPadding = + (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right) / 2; + if (isTablet) { + allAppsLeftRightPadding = + res.getDimensionPixelSize(R.dimen.all_apps_bottom_sheet_horizontal_padding); + int usedWidth = (allAppsCellWidthPx * numShownAllAppsColumns) - + (allAppsCellSpacePx.x * (numShownAllAppsColumns + 1)); - allAppsLeftRightPadding = Math.max(1, (availableWidthPx - usedWidth) / 2); + + (allAppsBorderSpacePx.x * (numShownAllAppsColumns - 1)) + + allAppsLeftRightPadding * 2; + allAppsLeftRightMargin = Math.max(1, (availableWidthPx - usedWidth) / 2); } else { allAppsLeftRightPadding = - desiredWorkspaceHorizontalMarginPx + cellLayoutPaddingLeftRightPx; + desiredWorkspaceHorizontalMarginPx + cellLayoutHorizontalPadding; } } @@ -609,11 +674,11 @@ public class DeviceProfile { private int updateAvailableDimensions(Resources res) { updateIconSize(1f, res); - Point workspacePadding = getTotalWorkspacePadding(); + updateWorkspacePadding(); // Check to see if the icons fit within the available height. - float usedHeight = getCellLayoutHeight(); - final int maxHeight = availableHeightPx - workspacePadding.y; + float usedHeight = getCellLayoutHeightSpecification(); + final int maxHeight = getCellLayoutHeight(); float extraHeight = Math.max(0, maxHeight - usedHeight); float scaleY = maxHeight / usedHeight; boolean shouldScale = scaleY < 1f; @@ -623,10 +688,8 @@ public class DeviceProfile { // We scale to fit the cellWidth and cellHeight in the available space. // The benefit of scalable grids is that we can get consistent aspect ratios between // devices. - int numColumns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns; - float usedWidth = (cellWidthPx * numColumns) - + (cellLayoutBorderSpacePx.x * (numColumns - 1)) - + (desiredWorkspaceHorizontalMarginPx * 2); + float usedWidth = + getCellLayoutWidthSpecification() + (desiredWorkspaceHorizontalMarginPx * 2); // We do not subtract padding here, as we also scale the workspace padding if needed. scaleX = availableWidthPx / usedWidth; shouldScale = true; @@ -635,15 +698,22 @@ public class DeviceProfile { if (shouldScale) { float scale = Math.min(scaleX, scaleY); updateIconSize(scale, res); - extraHeight = Math.max(0, maxHeight - getCellLayoutHeight()); + extraHeight = Math.max(0, maxHeight - getCellLayoutHeightSpecification()); } updateAvailableFolderCellDimensions(res); return Math.round(extraHeight); } - private int getCellLayoutHeight() { - return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacePx.y * (inv.numRows - 1)); + private int getCellLayoutHeightSpecification() { + return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacePx.y * (inv.numRows - 1)) + + cellLayoutPaddingPx.top + cellLayoutPaddingPx.bottom; + } + + private int getCellLayoutWidthSpecification() { + int numColumns = getPanelCount() * inv.numColumns; + return (cellWidthPx * numColumns) + (cellLayoutBorderSpacePx.x * (numColumns - 1)) + + cellLayoutPaddingPx.left + cellLayoutPaddingPx.right; } /** @@ -665,7 +735,7 @@ public class DeviceProfile { iconTextSizePx = (int) (pxFromSp(invIconTextSizeSp, mMetrics) * iconScale); iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * iconScale); - cellLayoutBorderSpacePx = getCellLayoutBorderSpaceScaled(inv, scale); + cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv, scale); if (isScalableGrid) { cellWidthPx = pxFromDp(inv.minCellSize[mTypeIndex].x, mMetrics, scale); @@ -692,46 +762,68 @@ public class DeviceProfile { } // All apps - if (numShownAllAppsColumns != inv.numColumns) { - allAppsIconSizePx = - pxFromDp(inv.iconSize[InvariantDeviceProfile.INDEX_ALL_APPS], mMetrics); - allAppsIconTextSizePx = - pxFromSp(inv.iconTextSize[InvariantDeviceProfile.INDEX_ALL_APPS], mMetrics); - allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx; - autoResizeAllAppsCells(); - } else { - allAppsIconSizePx = iconSizePx; - allAppsIconTextSizePx = iconTextSizePx; - allAppsIconDrawablePaddingPx = iconDrawablePaddingPx; - allAppsCellHeightPx = getCellSize().y; - } - allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx; - updateAllAppsWidth(); + updateAllAppsIconSize(scale, res); - if (isVerticalLayout) { - hideWorkspaceLabelsIfNotEnoughSpace(); - } - - // Hotseat updateHotseatIconSize(iconSizePx); - if (!isVerticalLayout) { - int expectedWorkspaceHeight = availableHeightPx - hotseatBarSizePx - - workspacePageIndicatorHeight - edgeMarginPx; - float minRequiredHeight = dropTargetBarSizePx + workspaceSpringLoadedBottomSpace; - workspaceSpringLoadShrinkFactor = Math.min( - res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f, - 1 - (minRequiredHeight / expectedWorkspaceHeight)); - } else { - workspaceSpringLoadShrinkFactor = - res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; - } - // Folder icon folderIconSizePx = IconNormalizer.getNormalizedCircleSize(iconSizePx); folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2; } + /** + * Hotseat width spans a certain number of columns on scalable grids. + * This method calculates the space between the icons to achieve that width. + */ + private int calculateHotseatBorderSpace() { + if (!isScalableGrid) return 0; + //TODO(http://b/228998082) remove this when 3 button spaces are fixed + if (areNavButtonsInline) { + return pxFromDp(inv.hotseatBorderSpaces[mTypeIndex], mMetrics); + } else { + int columns = inv.hotseatColumnSpan[mTypeIndex]; + float hotseatWidthPx = getIconToIconWidthForColumns(columns); + float hotseatIconsTotalPx = iconSizePx * numShownHotseatIcons; + return (int) (hotseatWidthPx - hotseatIconsTotalPx) / (numShownHotseatIcons - 1); + } + } + + + /** + * Updates the iconSize for allApps* variants. + */ + private void updateAllAppsIconSize(float scale, Resources res) { + allAppsBorderSpacePx = new Point( + pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics, scale), + pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics, scale)); + // AllApps cells don't have real space between cells, + // so we add the border space to the cell height + allAppsCellHeightPx = pxFromDp(inv.allAppsCellSize[mTypeIndex].y, mMetrics, scale) + + allAppsBorderSpacePx.y; + // but width is just the cell, + // the border is added in #updateAllAppsContainerWidth + allAppsCellWidthPx = pxFromDp(inv.allAppsCellSize[mTypeIndex].x, mMetrics, scale); + if (isScalableGrid) { + allAppsIconSizePx = + pxFromDp(inv.allAppsIconSize[mTypeIndex], mMetrics, scale); + allAppsIconTextSizePx = + pxFromSp(inv.allAppsIconTextSize[mTypeIndex], mMetrics, scale); + allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx; + } else { + float invIconSizeDp = inv.allAppsIconSize[mTypeIndex]; + float invIconTextSizeSp = inv.allAppsIconTextSize[mTypeIndex]; + allAppsIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale)); + allAppsIconTextSizePx = (int) (pxFromSp(invIconTextSizeSp, mMetrics) * scale); + allAppsIconDrawablePaddingPx = + res.getDimensionPixelSize(R.dimen.all_apps_icon_drawable_padding); + } + + updateAllAppsContainerWidth(res); + if (isVerticalBarLayout()) { + hideWorkspaceLabelsIfNotEnoughSpace(); + } + } + private void updateAvailableFolderCellDimensions(Resources res) { updateFolderCellSize(1f, res); @@ -763,11 +855,11 @@ public class DeviceProfile { private void updateFolderCellSize(float scale, Resources res) { float invIconSizeDp = isVerticalBarLayout() - ? inv.iconSize[InvariantDeviceProfile.INDEX_LANDSCAPE] - : inv.iconSize[InvariantDeviceProfile.INDEX_DEFAULT]; + ? inv.iconSize[INDEX_LANDSCAPE] + : inv.iconSize[INDEX_DEFAULT]; folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale)); folderChildTextSizePx = - pxFromSp(inv.iconTextSize[InvariantDeviceProfile.INDEX_DEFAULT], mMetrics, scale); + pxFromSp(inv.iconTextSize[INDEX_DEFAULT], mMetrics, scale); folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale); int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx); @@ -799,7 +891,6 @@ public class DeviceProfile { public void updateInsets(Rect insets) { mInsets.set(insets); - updateWorkspacePadding(); } /** @@ -819,30 +910,97 @@ public class DeviceProfile { result = new Point(); } - // Since we are only concerned with the overall padding, layout direction does - // not matter. - Point padding = getTotalWorkspacePadding(); - - int numColumns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns; - int screenWidthPx = getWorkspaceWidth(padding); - result.x = calculateCellWidth(screenWidthPx, cellLayoutBorderSpacePx.x, numColumns); - result.y = calculateCellHeight(availableHeightPx - padding.y - - cellLayoutBottomPaddingPx, cellLayoutBorderSpacePx.y, inv.numRows); + int shortcutAndWidgetContainerWidth = + getCellLayoutWidth() - (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right); + result.x = calculateCellWidth(shortcutAndWidgetContainerWidth, cellLayoutBorderSpacePx.x, + inv.numColumns); + int shortcutAndWidgetContainerHeight = + getCellLayoutHeight() - (cellLayoutPaddingPx.top + cellLayoutPaddingPx.bottom); + result.y = calculateCellHeight(shortcutAndWidgetContainerHeight, cellLayoutBorderSpacePx.y, + inv.numRows); return result; } - public int getWorkspaceWidth() { - return getWorkspaceWidth(getTotalWorkspacePadding()); + /** + * Gets the number of panels within the workspace. + */ + public int getPanelCount() { + return isTwoPanels ? 2 : 1; } - public int getWorkspaceWidth(Point workspacePadding) { - int cellLayoutTotalPadding = - isTwoPanels ? 4 * cellLayoutPaddingLeftRightPx : 2 * cellLayoutPaddingLeftRightPx; - return availableWidthPx - workspacePadding.x - cellLayoutTotalPadding; + /** + * Gets the space in px from the bottom of last item in the vertical-bar hotseat to the + * bottom of the screen. + */ + public int getVerticalHotseatLastItemBottomOffset() { + int cellHeight = calculateCellHeight( + heightPx - mHotseatPadding.top - mHotseatPadding.bottom, hotseatBorderSpace, + numShownHotseatIcons); + int hotseatSize = (cellHeight * numShownHotseatIcons) + + (hotseatBorderSpace * (numShownHotseatIcons - 1)); + int extraHotseatEndSpacing = (heightPx - hotseatSize) / 2; + int extraIconEndSpacing = (cellHeight - iconSizePx) / 2; + return extraHotseatEndSpacing + extraIconEndSpacing + mHotseatPadding.bottom; + } + + /** + * Gets the scaled top of the workspace in px for the spring-loaded edit state. + */ + public float getCellLayoutSpringLoadShrunkTop() { + workspaceSpringLoadShrunkTop = mInsets.top + dropTargetBarTopMarginPx + dropTargetBarSizePx + + dropTargetBarBottomMarginPx; + return workspaceSpringLoadShrunkTop; + } + + /** + * Gets the scaled bottom of the workspace in px for the spring-loaded edit state. + */ + private float getCellLayoutSpringLoadShrunkBottom() { + int topOfHotseat = hotseatBarSizePx + springLoadedHotseatBarTopMarginPx; + workspaceSpringLoadShrunkBottom = + heightPx - (isVerticalBarLayout() ? getVerticalHotseatLastItemBottomOffset() + : topOfHotseat); + return workspaceSpringLoadShrunkBottom; + } + + /** + * Gets the scale of the workspace for the spring-loaded edit state. + */ + public float getWorkspaceSpringLoadScale() { + float scale = (getCellLayoutSpringLoadShrunkBottom() - getCellLayoutSpringLoadShrunkTop()) + / getCellLayoutHeight(); + scale = Math.min(scale, 1f); + + // Reduce scale if next pages would not be visible after scaling the workspace + int workspaceWidth = availableWidthPx; + float scaledWorkspaceWidth = workspaceWidth * scale; + float maxAvailableWidth = workspaceWidth - (2 * workspaceSpringLoadedMinNextPageVisiblePx); + if (scaledWorkspaceWidth > maxAvailableWidth) { + scale *= maxAvailableWidth / scaledWorkspaceWidth; + } + return scale; + } + + /** + * Gets the width of a single Cell Layout, aka a single panel within a Workspace. + * + *

This is the width of a Workspace, less its horizontal padding. Note that two-panel + * layouts have two Cell Layouts per workspace. + */ + public int getCellLayoutWidth() { + return (availableWidthPx - getTotalWorkspacePadding().x) / getPanelCount(); + } + + /** + * Gets the height of a single Cell Layout, aka a single panel within a Workspace. + * + *

This is the height of a Workspace, less its vertical padding. + */ + public int getCellLayoutHeight() { + return availableHeightPx - getTotalWorkspacePadding().y; } public Point getTotalWorkspacePadding() { - updateWorkspacePadding(); return new Point(workspacePadding.left + workspacePadding.right, workspacePadding.top + workspacePadding.bottom); } @@ -868,12 +1026,26 @@ public class DeviceProfile { int hotseatTop = hotseatBarSizePx; int paddingBottom = hotseatTop + workspacePageIndicatorHeight + workspaceBottomPadding - mWorkspacePageIndicatorOverlapWorkspace; + int paddingTop = workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx); + int paddingSide = desiredWorkspaceHorizontalMarginPx; - padding.set(desiredWorkspaceHorizontalMarginPx, - workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx), - desiredWorkspaceHorizontalMarginPx, - paddingBottom); + padding.set(paddingSide, paddingTop, paddingSide, paddingBottom); } + insetPadding(workspacePadding, cellLayoutPaddingPx); + } + + private void insetPadding(Rect paddings, Rect insets) { + insets.left = Math.min(insets.left, paddings.left); + paddings.left -= insets.left; + + insets.top = Math.min(insets.top, paddings.top); + paddings.top -= insets.top; + + insets.right = Math.min(insets.right, paddings.right); + paddings.right -= insets.right; + + insets.bottom = Math.min(insets.bottom, paddings.bottom); + paddings.bottom -= insets.bottom; } /** @@ -881,37 +1053,62 @@ public class DeviceProfile { */ public Rect getHotseatLayoutPadding(Context context) { if (isVerticalBarLayout()) { + // The hotseat icons will be placed in the middle of the hotseat cells. + // Changing the hotseatCellHeightPx is not affecting hotseat icon positions + // in vertical bar layout. + // Workspace icons are moved up by a small factor. The variable diffOverlapFactor + // is set to account for that difference. + float diffOverlapFactor = iconSizePx * (ICON_OVERLAP_FACTOR - 1) / 2; + int paddingTop = Math.max((int) (mInsets.top + cellLayoutPaddingPx.top + - diffOverlapFactor), 0); + int paddingBottom = Math.max((int) (mInsets.bottom + cellLayoutPaddingPx.bottom + + diffOverlapFactor), 0); + if (isSeascape()) { - mHotseatPadding.set(mInsets.left + hotseatBarSidePaddingStartPx, - mInsets.top, hotseatBarSidePaddingEndPx, mInsets.bottom); + mHotseatPadding.set(mInsets.left + hotseatBarSidePaddingStartPx, paddingTop, + hotseatBarSidePaddingEndPx, paddingBottom); } else { - mHotseatPadding.set(hotseatBarSidePaddingEndPx, mInsets.top, - mInsets.right + hotseatBarSidePaddingStartPx, mInsets.bottom); + mHotseatPadding.set(hotseatBarSidePaddingEndPx, paddingTop, + mInsets.right + hotseatBarSidePaddingStartPx, paddingBottom); } } else if (isTaskbarPresent) { - int hotseatHeight = workspacePadding.bottom; - int taskbarOffset = getTaskbarOffsetY(); - int hotseatTopDiff = hotseatHeight - taskbarOffset; + // Center the QSB vertically with hotseat + int hotseatBottomPadding = getHotseatBottomPadding(); + int hotseatTopPadding = + workspacePadding.bottom - hotseatBottomPadding - hotseatCellHeightPx; + // Push icons to the side + int additionalQsbSpace = isQsbInline ? qsbWidth + hotseatBorderSpace : 0; + int requiredWidth = iconSizePx * numShownHotseatIcons + + hotseatBorderSpace * (numShownHotseatIcons - 1) + + additionalQsbSpace; int endOffset = ApiWrapper.getHotseatEndOffset(context); - int requiredWidth = iconSizePx * numShownHotseatIcons; + int hotseatWidth = Math.min(requiredWidth, availableWidthPx - endOffset); + int sideSpacing = (availableWidthPx - hotseatWidth) / 2; - Resources res = context.getResources(); - float taskbarIconSize = res.getDimension(R.dimen.taskbar_icon_size); - float taskbarIconSpacing = 2 * res.getDimension(R.dimen.taskbar_icon_spacing); - int maxSize = (int) (requiredWidth - * (taskbarIconSize + taskbarIconSpacing) / taskbarIconSize); - int hotseatSize = Math.min(maxSize, availableWidthPx - endOffset); - int sideSpacing = (availableWidthPx - hotseatSize) / 2; - mHotseatPadding.set(sideSpacing, hotseatTopDiff, sideSpacing, taskbarOffset); + mHotseatPadding.set(sideSpacing, hotseatTopPadding, sideSpacing, hotseatBottomPadding); + + boolean isRtl = Utilities.isRtl(context.getResources()); + if (isRtl) { + mHotseatPadding.right += additionalQsbSpace; + } else { + mHotseatPadding.left += additionalQsbSpace; + } if (endOffset > sideSpacing) { - int diff = Utilities.isRtl(context.getResources()) + int diff = isRtl ? sideSpacing - endOffset : endOffset - sideSpacing; mHotseatPadding.left -= diff; mHotseatPadding.right += diff; } + } else if (isScalableGrid) { + int sideSpacing = (availableWidthPx - qsbWidth) / 2; + mHotseatPadding.set(sideSpacing, + hotseatBarTopPaddingPx, + sideSpacing, + hotseatBarSizePx - hotseatCellHeightPx - hotseatBarTopPaddingPx + + mInsets.bottom); } else { // We want the edges of the hotseat to line up with the edges of the workspace, but the // icons in the hotseat are a different size, and so don't line up perfectly. To account @@ -920,14 +1117,12 @@ public class DeviceProfile { float workspaceCellWidth = (float) widthPx / inv.numColumns; float hotseatCellWidth = (float) widthPx / numShownHotseatIcons; int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2); - mHotseatPadding.set( - hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx - + mInsets.left, - hotseatBarTopPaddingPx, - hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx + mHotseatPadding.set(hotseatAdjustment + workspacePadding.left + cellLayoutPaddingPx.left + + mInsets.left, hotseatBarTopPaddingPx, + hotseatAdjustment + workspacePadding.right + cellLayoutPaddingPx.right + mInsets.right, hotseatBarSizePx - hotseatCellHeightPx - hotseatBarTopPaddingPx - + cellLayoutBottomPaddingPx + mInsets.bottom); + + mInsets.bottom); } return mHotseatPadding; } @@ -936,6 +1131,10 @@ public class DeviceProfile { * Returns the number of pixels the QSB is translated from the bottom of the screen. */ public int getQsbOffsetY() { + if (isQsbInline) { + return hotseatBarBottomPaddingPx; + } + int freeSpace = isTaskbarPresent ? workspacePadding.bottom : hotseatBarSizePx - hotseatCellHeightPx - hotseatQsbHeight; @@ -944,16 +1143,45 @@ public class DeviceProfile { // Note that taskbarSize = 0 unless isTaskbarPresent. return Math.min(qsbBottomMarginPx + taskbarSize, freeSpace); } else { - return (int) (freeSpace * QSB_CENTER_FACTOR) + return (int) (freeSpace * mQsbCenterFactor) + (isTaskbarPresent ? taskbarSize : mInsets.bottom); } } + private int getHotseatBottomPadding() { + if (isQsbInline) { + return getQsbOffsetY() - (Math.abs(hotseatQsbHeight - hotseatCellHeightPx) / 2); + } else { + return (getQsbOffsetY() - taskbarSize) / 2; + } + } + /** * Returns the number of pixels the taskbar is translated from the bottom of the screen. */ public int getTaskbarOffsetY() { - return (getQsbOffsetY() - taskbarSize) / 2; + int taskbarIconBottomSpace = (taskbarSize - iconSizePx) / 2; + int launcherIconBottomSpace = + Math.min((hotseatCellHeightPx - iconSizePx) / 2, gridVisualizationPaddingY); + return getHotseatBottomPadding() + launcherIconBottomSpace - taskbarIconBottomSpace; + } + + /** + * Returns the number of pixels required below OverviewActions excluding insets. + */ + public int getOverviewActionsClaimedSpaceBelow() { + if (isTaskbarPresent && !isGestureMode) { + // Align vertically to where nav buttons are. + return ((taskbarSize - overviewActionsHeight) / 2) + getTaskbarOffsetY(); + } + + return isTaskbarPresent ? stashedTaskbarSize : mInsets.bottom; + } + + /** Gets the space that the overview actions will take, including bottom margin. */ + public int getOverviewActionsClaimedSpace() { + return overviewActionsTopMarginPx + overviewActionsHeight + + getOverviewActionsClaimedSpaceBelow(); } /** @@ -1003,6 +1231,8 @@ public class DeviceProfile { .getInfo().rotation == Surface.ROTATION_270; if (mIsSeascape != isSeascape) { mIsSeascape = isSeascape; + // Hotseat changing sides requires updating workspace left/right paddings + updateWorkspacePadding(); return true; } } @@ -1045,6 +1275,7 @@ public class DeviceProfile { writer.println(prefix + "\tisPhone:" + isPhone); writer.println(prefix + "\ttransposeLayoutWithOrientation:" + transposeLayoutWithOrientation); + writer.println(prefix + "\tisGestureMode:" + isGestureMode); writer.println(prefix + "\tisLandscape:" + isLandscape); writer.println(prefix + "\tisMultiWindowMode:" + isMultiWindowMode); @@ -1054,16 +1285,21 @@ public class DeviceProfile { writer.println(prefix + pxToDpStr("windowY", windowY)); writer.println(prefix + pxToDpStr("widthPx", widthPx)); writer.println(prefix + pxToDpStr("heightPx", heightPx)); - writer.println(prefix + pxToDpStr("availableWidthPx", availableWidthPx)); writer.println(prefix + pxToDpStr("availableHeightPx", availableHeightPx)); + writer.println(prefix + pxToDpStr("mInsets.left", mInsets.left)); + writer.println(prefix + pxToDpStr("mInsets.top", mInsets.top)); + writer.println(prefix + pxToDpStr("mInsets.right", mInsets.right)); + writer.println(prefix + pxToDpStr("mInsets.bottom", mInsets.bottom)); writer.println(prefix + "\taspectRatio:" + aspectRatio); writer.println(prefix + "\tisScalableGrid:" + isScalableGrid); - writer.println(prefix + "\tinv.numColumns: " + inv.numColumns); writer.println(prefix + "\tinv.numRows: " + inv.numRows); + writer.println(prefix + "\tinv.numColumns: " + inv.numColumns); + writer.println(prefix + "\tinv.numSearchContainerColumns: " + + inv.numSearchContainerColumns); writer.println(prefix + "\tminCellSize: " + inv.minCellSize[mTypeIndex] + "dp"); @@ -1077,6 +1313,11 @@ public class DeviceProfile { cellLayoutBorderSpacePx.x)); writer.println(prefix + pxToDpStr("cellLayoutBorderSpacePx Vertical", cellLayoutBorderSpacePx.y)); + writer.println(prefix + pxToDpStr("cellLayoutPaddingPx.left", cellLayoutPaddingPx.left)); + writer.println(prefix + pxToDpStr("cellLayoutPaddingPx.top", cellLayoutPaddingPx.top)); + writer.println(prefix + pxToDpStr("cellLayoutPaddingPx.right", cellLayoutPaddingPx.right)); + writer.println( + prefix + pxToDpStr("cellLayoutPaddingPx.bottom", cellLayoutPaddingPx.bottom)); writer.println(prefix + pxToDpStr("iconSizePx", iconSizePx)); writer.println(prefix + pxToDpStr("iconTextSizePx", iconTextSizePx)); @@ -1095,14 +1336,23 @@ public class DeviceProfile { writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx Vertical", folderCellLayoutBorderSpacePx.y)); + writer.println(prefix + pxToDpStr("bottomSheetTopPadding", bottomSheetTopPadding)); + + writer.println(prefix + pxToDpStr("allAppsShiftRange", allAppsShiftRange)); + writer.println(prefix + pxToDpStr("allAppsTopPadding", allAppsTopPadding)); writer.println(prefix + pxToDpStr("allAppsIconSizePx", allAppsIconSizePx)); writer.println(prefix + pxToDpStr("allAppsIconTextSizePx", allAppsIconTextSizePx)); writer.println(prefix + pxToDpStr("allAppsIconDrawablePaddingPx", allAppsIconDrawablePaddingPx)); writer.println(prefix + pxToDpStr("allAppsCellHeightPx", allAppsCellHeightPx)); + writer.println(prefix + pxToDpStr("allAppsCellWidthPx", allAppsCellWidthPx)); + writer.println(prefix + pxToDpStr("allAppsBorderSpacePx", allAppsBorderSpacePx.x)); writer.println(prefix + "\tnumShownAllAppsColumns: " + numShownAllAppsColumns); + writer.println(prefix + pxToDpStr("allAppsLeftRightPadding", allAppsLeftRightPadding)); + writer.println(prefix + pxToDpStr("allAppsLeftRightMargin", allAppsLeftRightMargin)); writer.println(prefix + pxToDpStr("hotseatBarSizePx", hotseatBarSizePx)); + writer.println(prefix + "\tinv.hotseatColumnSpan: " + inv.hotseatColumnSpan[mTypeIndex]); writer.println(prefix + pxToDpStr("hotseatCellHeightPx", hotseatCellHeightPx)); writer.println(prefix + pxToDpStr("hotseatBarTopPaddingPx", hotseatBarTopPaddingPx)); writer.println(prefix + pxToDpStr("hotseatBarBottomPaddingPx", hotseatBarBottomPaddingPx)); @@ -1110,7 +1360,16 @@ public class DeviceProfile { hotseatBarSidePaddingStartPx)); writer.println(prefix + pxToDpStr("hotseatBarSidePaddingEndPx", hotseatBarSidePaddingEndPx)); + writer.println(prefix + pxToDpStr("springLoadedHotseatBarTopMarginPx", + springLoadedHotseatBarTopMarginPx)); + writer.println(prefix + pxToDpStr("mHotseatPadding.top", mHotseatPadding.top)); + writer.println(prefix + pxToDpStr("mHotseatPadding.bottom", mHotseatPadding.bottom)); + writer.println(prefix + pxToDpStr("mHotseatPadding.left", mHotseatPadding.left)); + writer.println(prefix + pxToDpStr("mHotseatPadding.right", mHotseatPadding.right)); writer.println(prefix + "\tnumShownHotseatIcons: " + numShownHotseatIcons); + writer.println(prefix + pxToDpStr("hotseatBorderSpace", hotseatBorderSpace)); + writer.println(prefix + "\tisQsbInline: " + isQsbInline); + writer.println(prefix + pxToDpStr("qsbWidth", qsbWidth)); writer.println(prefix + "\tisTaskbarPresent:" + isTaskbarPresent); writer.println(prefix + "\tisTaskbarPresentInApps:" + isTaskbarPresentInApps); @@ -1136,12 +1395,48 @@ public class DeviceProfile { writer.println(prefix + pxToDpStr("workspaceTopPadding", workspaceTopPadding)); writer.println(prefix + pxToDpStr("workspaceBottomPadding", workspaceBottomPadding)); writer.println(prefix + pxToDpStr("extraHotseatBottomPadding", extraHotseatBottomPadding)); + + writer.println(prefix + pxToDpStr("overviewTaskMarginPx", overviewTaskMarginPx)); + writer.println(prefix + pxToDpStr("overviewTaskMarginGridPx", overviewTaskMarginGridPx)); + writer.println(prefix + pxToDpStr("overviewTaskIconSizePx", overviewTaskIconSizePx)); + writer.println(prefix + pxToDpStr("overviewTaskIconDrawableSizePx", + overviewTaskIconDrawableSizePx)); + writer.println(prefix + pxToDpStr("overviewTaskIconDrawableSizeGridPx", + overviewTaskIconDrawableSizeGridPx)); + writer.println(prefix + pxToDpStr("overviewTaskThumbnailTopMarginPx", + overviewTaskThumbnailTopMarginPx)); + writer.println(prefix + pxToDpStr("overviewActionsTopMarginPx", + overviewActionsTopMarginPx)); + writer.println(prefix + pxToDpStr("overviewActionsHeight", + overviewActionsHeight)); + writer.println(prefix + pxToDpStr("overviewActionsButtonSpacing", + overviewActionsButtonSpacing)); + writer.println(prefix + pxToDpStr("overviewPageSpacing", overviewPageSpacing)); + writer.println(prefix + pxToDpStr("overviewRowSpacing", overviewRowSpacing)); + writer.println(prefix + pxToDpStr("overviewGridSideMargin", overviewGridSideMargin)); + + writer.println(prefix + pxToDpStr("dropTargetBarTopMarginPx", dropTargetBarTopMarginPx)); + writer.println(prefix + pxToDpStr("dropTargetBarSizePx", dropTargetBarSizePx)); + writer.println( + prefix + pxToDpStr("dropTargetBarBottomMarginPx", dropTargetBarBottomMarginPx)); + + writer.println( + prefix + pxToDpStr("workspaceSpringLoadShrunkTop", workspaceSpringLoadShrunkTop)); + writer.println(prefix + pxToDpStr("workspaceSpringLoadShrunkBottom", + workspaceSpringLoadShrunkBottom)); + writer.println(prefix + pxToDpStr("workspaceSpringLoadedBottomSpace", + workspaceSpringLoadedBottomSpace)); + writer.println(prefix + pxToDpStr("workspaceSpringLoadedMinNextPageVisiblePx", + workspaceSpringLoadedMinNextPageVisiblePx)); + writer.println( + prefix + pxToDpStr("getWorkspaceSpringLoadScale()", getWorkspaceSpringLoadScale())); } - private static Context getContext(Context c, Info info, int orientation) { + private static Context getContext(Context c, Info info, int orientation, WindowBounds bounds) { Configuration config = new Configuration(c.getResources().getConfiguration()); config.orientation = orientation; - config.densityDpi = info.densityDpi; + config.densityDpi = info.getDensityDpi(); + config.smallestScreenWidthDp = (int) info.smallestSizeDp(bounds); return c.createConfigurationContext(config); } @@ -1159,6 +1454,35 @@ public class DeviceProfile { void onDeviceProfileChanged(DeviceProfile dp); } + /** Allows registering listeners for {@link DeviceProfile} changes. */ + public interface DeviceProfileListenable { + + /** The current device profile. */ + DeviceProfile getDeviceProfile(); + + /** Registered {@link OnDeviceProfileChangeListener} instances. */ + List getOnDeviceProfileChangeListeners(); + + /** Notifies listeners of a {@link DeviceProfile} change. */ + default void dispatchDeviceProfileChanged() { + DeviceProfile deviceProfile = getDeviceProfile(); + List listeners = getOnDeviceProfileChangeListeners(); + for (int i = listeners.size() - 1; i >= 0; i--) { + listeners.get(i).onDeviceProfileChanged(deviceProfile); + } + } + + /** Register listener for {@link DeviceProfile} changes. */ + default void addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) { + getOnDeviceProfileChangeListeners().add(listener); + } + + /** Unregister listener for {@link DeviceProfile} changes. */ + default void removeOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) { + getOnDeviceProfileChangeListeners().remove(listener); + } + } + public static class Builder { private Context mContext; private InvariantDeviceProfile mInv; @@ -1169,6 +1493,7 @@ public class DeviceProfile { private boolean mIsMultiWindowMode = false; private Boolean mTransposeLayoutWithOrientation; + private Boolean mIsGestureMode; public Builder(Context context, InvariantDeviceProfile inv, Info info) { mContext = context; @@ -1197,6 +1522,11 @@ public class DeviceProfile { return this; } + public Builder setGestureMode(boolean isGestureMode) { + mIsGestureMode = isGestureMode; + return this; + } + public DeviceProfile build() { if (mWindowBounds == null) { throw new IllegalArgumentException("Window bounds not set"); @@ -1204,8 +1534,11 @@ public class DeviceProfile { if (mTransposeLayoutWithOrientation == null) { mTransposeLayoutWithOrientation = !mInfo.isTablet(mWindowBounds); } - return new DeviceProfile(mContext, mInv, mInfo, mWindowBounds, - mIsMultiWindowMode, mTransposeLayoutWithOrientation, mUseTwoPanels); + if (mIsGestureMode == null) { + mIsGestureMode = DisplayController.getNavigationMode(mContext).hasGestures; + } + return new DeviceProfile(mContext, mInv, mInfo, mWindowBounds, mIsMultiWindowMode, + mTransposeLayoutWithOrientation, mUseTwoPanels, mIsGestureMode); } } diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java index 9fb14f68cb..d908440bc8 100644 --- a/src/com/android/launcher3/DropTargetBar.java +++ b/src/com/android/launcher3/DropTargetBar.java @@ -17,8 +17,6 @@ package com.android.launcher3; import static com.android.launcher3.ButtonDropTarget.TOOLTIP_DEFAULT; -import static com.android.launcher3.ButtonDropTarget.TOOLTIP_LEFT; -import static com.android.launcher3.ButtonDropTarget.TOOLTIP_RIGHT; import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility; import android.animation.TimeInterpolator; @@ -53,6 +51,8 @@ public class DropTargetBar extends FrameLayout private final Runnable mFadeAnimationEndRunnable = () -> updateVisibility(DropTargetBar.this); + private final Launcher mLauncher; + @ViewDebug.ExportedProperty(category = "launcher") protected boolean mDeferOnDragEnd; @@ -60,16 +60,19 @@ public class DropTargetBar extends FrameLayout protected boolean mVisible = false; private ButtonDropTarget[] mDropTargets; + private ButtonDropTarget[] mTempTargets; private ViewPropertyAnimator mCurrentAnimation; private boolean mIsVertical = true; public DropTargetBar(Context context, AttributeSet attrs) { super(context, attrs); + mLauncher = Launcher.getLauncher(context); } public DropTargetBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mLauncher = Launcher.getLauncher(context); } @Override @@ -80,12 +83,13 @@ public class DropTargetBar extends FrameLayout mDropTargets[i] = (ButtonDropTarget) getChildAt(i); mDropTargets[i].setDropTargetBar(this); } + mTempTargets = new ButtonDropTarget[getChildCount()]; } @Override public void setInsets(Rect insets) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); mIsVertical = grid.isVerticalBarLayout(); lp.leftMargin = insets.left; @@ -94,34 +98,37 @@ public class DropTargetBar extends FrameLayout lp.rightMargin = insets.right; int tooltipLocation = TOOLTIP_DEFAULT; - if (grid.isVerticalBarLayout()) { - lp.width = grid.dropTargetBarSizePx; - lp.height = grid.availableHeightPx - 2 * grid.edgeMarginPx; - lp.gravity = grid.isSeascape() ? Gravity.RIGHT : Gravity.LEFT; - tooltipLocation = grid.isSeascape() ? TOOLTIP_LEFT : TOOLTIP_RIGHT; + int horizontalMargin; + if (grid.isTablet) { + // XXX: If the icon size changes across orientations, we will have to take + // that into account here too. + horizontalMargin = ((grid.widthPx - 2 * grid.edgeMarginPx + - (grid.inv.numColumns * grid.cellWidthPx)) + / (2 * (grid.inv.numColumns + 1))) + + grid.edgeMarginPx; } else { - int gap; - if (grid.isTablet) { - // XXX: If the icon size changes across orientations, we will have to take - // that into account here too. - gap = ((grid.widthPx - 2 * grid.edgeMarginPx - - (grid.inv.numColumns * grid.cellWidthPx)) - / (2 * (grid.inv.numColumns + 1))) - + grid.edgeMarginPx; - } else { - gap = getContext().getResources() - .getDimensionPixelSize(R.dimen.drop_target_bar_margin_horizontal); - } - lp.width = grid.availableWidthPx - 2 * gap; - - lp.topMargin += grid.edgeMarginPx; - lp.height = grid.dropTargetBarSizePx; - lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + horizontalMargin = getContext().getResources() + .getDimensionPixelSize(R.dimen.drop_target_bar_margin_horizontal); } + lp.topMargin += grid.dropTargetBarTopMarginPx; + lp.bottomMargin += grid.dropTargetBarBottomMarginPx; + lp.width = grid.availableWidthPx - 2 * horizontalMargin; + if (mIsVertical) { + lp.leftMargin = (grid.widthPx - lp.width) / 2; + lp.rightMargin = (grid.widthPx - lp.width) / 2; + } + lp.height = grid.dropTargetBarSizePx; + lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + + DeviceProfile dp = mLauncher.getDeviceProfile(); + int horizontalPadding = dp.dropTargetHorizontalPaddingPx; + int verticalPadding = dp.dropTargetVerticalPaddingPx; setLayoutParams(lp); for (ButtonDropTarget button : mDropTargets) { button.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.dropTargetTextSizePx); button.setToolTipLocation(tooltipLocation); + button.setPadding(horizontalPadding, verticalPadding, horizontalPadding, + verticalPadding); } } @@ -137,35 +144,76 @@ public class DropTargetBar extends FrameLayout protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); + int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - int visibleCount = getVisibleButtonsCount(); - if (visibleCount == 0) { - // do nothing - } else if (mIsVertical) { - int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + int visibleCount = getVisibleButtons(mTempTargets); + if (visibleCount == 1) { + int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST); - for (ButtonDropTarget button : mDropTargets) { - if (button.getVisibility() != GONE) { - button.setTextVisible(false); - button.measure(widthSpec, heightSpec); - } - } - } else { - int availableWidth = width / visibleCount; - boolean textVisible = true; - for (ButtonDropTarget buttons : mDropTargets) { - if (buttons.getVisibility() != GONE) { - textVisible = textVisible && !buttons.isTextTruncated(availableWidth); - } + ButtonDropTarget firstButton = mTempTargets[0]; + firstButton.setTextVisible(true); + firstButton.setIconVisible(true); + firstButton.measure(widthSpec, heightSpec); + } else if (visibleCount == 2) { + DeviceProfile dp = mLauncher.getDeviceProfile(); + int verticalPadding = dp.dropTargetVerticalPaddingPx; + int horizontalPadding = dp.dropTargetHorizontalPaddingPx; + + ButtonDropTarget firstButton = mTempTargets[0]; + firstButton.setTextVisible(true); + firstButton.setIconVisible(true); + firstButton.setTextMultiLine(false); + // Reset second button padding in case it was previously changed to multi-line text. + firstButton.setPadding(horizontalPadding, verticalPadding, horizontalPadding, + verticalPadding); + + ButtonDropTarget secondButton = mTempTargets[1]; + secondButton.setTextVisible(true); + secondButton.setIconVisible(true); + secondButton.setTextMultiLine(false); + // Reset second button padding in case it was previously changed to multi-line text. + secondButton.setPadding(horizontalPadding, verticalPadding, horizontalPadding, + verticalPadding); + + float scale = dp.getWorkspaceSpringLoadScale(); + int scaledPanelWidth = (int) (dp.getCellLayoutWidth() * scale); + + int availableWidth; + if (dp.isTwoPanels) { + // Both buttons for two panel fit to the width of one Cell Layout (less + // half of the center gap between the buttons). + int halfButtonGap = dp.dropTargetGapPx / 2; + availableWidth = scaledPanelWidth - halfButtonGap / 2; + } else { + // Both buttons plus the button gap do not display past the edge of the scaled + // workspace, less a pre-defined gap from the edge of the workspace. + availableWidth = scaledPanelWidth - dp.dropTargetGapPx + - 2 * dp.dropTargetButtonWorkspaceEdgeGapPx; } int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - for (ButtonDropTarget button : mDropTargets) { - if (button.getVisibility() != GONE) { - button.setTextVisible(textVisible); - button.measure(widthSpec, heightSpec); + firstButton.measure(widthSpec, heightSpec); + if (!mIsVertical) { + // Remove icons and put the button's text on two lines if text is truncated. + if (firstButton.isTextTruncated(availableWidth)) { + firstButton.setIconVisible(false); + firstButton.setTextMultiLine(true); + firstButton.setPadding(horizontalPadding, verticalPadding / 2, + horizontalPadding, verticalPadding / 2); + } + } + + if (!dp.isTwoPanels) { + availableWidth -= firstButton.getMeasuredWidth() + dp.dropTargetGapPx; + widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST); + } + secondButton.measure(widthSpec, heightSpec); + if (!mIsVertical) { + if (secondButton.isTextTruncated(availableWidth)) { + secondButton.setIconVisible(false); + secondButton.setTextMultiLine(true); + secondButton.setPadding(horizontalPadding, verticalPadding / 2, + horizontalPadding, verticalPadding / 2); } } } @@ -174,41 +222,66 @@ public class DropTargetBar extends FrameLayout @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int visibleCount = getVisibleButtonsCount(); + int visibleCount = getVisibleButtons(mTempTargets); if (visibleCount == 0) { - // do nothing - } else if (mIsVertical) { - int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap); - int start = gap; - int end; + return; + } - for (ButtonDropTarget button : mDropTargets) { - if (button.getVisibility() != GONE) { - end = start + button.getMeasuredHeight(); - button.layout(0, start, button.getMeasuredWidth(), end); - start = end + gap; - } - } + DeviceProfile dp = mLauncher.getDeviceProfile(); + // Center vertical bar over scaled workspace, accounting for hotseat offset. + float scale = dp.getWorkspaceSpringLoadScale(); + Workspace ws = mLauncher.getWorkspace(); + int barCenter; + if (dp.isTwoPanels) { + barCenter = (right - left) / 2; } else { - int frameSize = (right - left) / visibleCount; + int workspaceCenter = (ws.getLeft() + ws.getRight()) / 2; + int cellLayoutCenter = ((dp.getInsets().left + dp.workspacePadding.left) + (dp.widthPx + - dp.getInsets().right - dp.workspacePadding.right)) / 2; + int cellLayoutCenterOffset = (int) ((cellLayoutCenter - workspaceCenter) * scale); + barCenter = workspaceCenter + cellLayoutCenterOffset - left; + } - int start = frameSize / 2; - int halfWidth; - for (ButtonDropTarget button : mDropTargets) { - if (button.getVisibility() != GONE) { - halfWidth = button.getMeasuredWidth() / 2; - button.layout(start - halfWidth, 0, - start + halfWidth, button.getMeasuredHeight()); - start = start + frameSize; - } + if (visibleCount == 1) { + ButtonDropTarget button = mTempTargets[0]; + button.layout(barCenter - (button.getMeasuredWidth() / 2), 0, + barCenter + (button.getMeasuredWidth() / 2), button.getMeasuredHeight()); + } else if (visibleCount == 2) { + int buttonGap = dp.dropTargetGapPx; + + ButtonDropTarget leftButton = mTempTargets[0]; + ButtonDropTarget rightButton = mTempTargets[1]; + if (dp.isTwoPanels) { + leftButton.layout(barCenter - leftButton.getMeasuredWidth() - (buttonGap / 2), 0, + barCenter - (buttonGap / 2), leftButton.getMeasuredHeight()); + rightButton.layout(barCenter + (buttonGap / 2), 0, + barCenter + (buttonGap / 2) + rightButton.getMeasuredWidth(), + rightButton.getMeasuredHeight()); + } else { + int scaledPanelWidth = (int) (dp.getCellLayoutWidth() * scale); + + int leftButtonWidth = leftButton.getMeasuredWidth(); + int rightButtonWidth = rightButton.getMeasuredWidth(); + int extraSpace = scaledPanelWidth - leftButtonWidth - rightButtonWidth - buttonGap; + + int leftButtonStart = barCenter - (scaledPanelWidth / 2) + extraSpace / 2; + int leftButtonEnd = leftButtonStart + leftButtonWidth; + int rightButtonStart = leftButtonEnd + buttonGap; + int rightButtonEnd = rightButtonStart + rightButtonWidth; + + leftButton.layout(leftButtonStart, 0, leftButtonEnd, + leftButton.getMeasuredHeight()); + rightButton.layout(rightButtonStart, 0, rightButtonEnd, + rightButton.getMeasuredHeight()); } } } - private int getVisibleButtonsCount() { + private int getVisibleButtons(ButtonDropTarget[] outVisibleButtons) { int visibleCount = 0; - for (ButtonDropTarget buttons : mDropTargets) { - if (buttons.getVisibility() != GONE) { + for (ButtonDropTarget button : mDropTargets) { + if (button.getVisibility() != GONE) { + outVisibleButtons[visibleCount] = button; visibleCount++; } } @@ -269,7 +342,7 @@ public class DropTargetBar extends FrameLayout } public ButtonDropTarget[] getDropTargets() { - return mDropTargets; + return getVisibility() == View.VISIBLE ? mDropTargets : new ButtonDropTarget[0]; } @Override diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java index 3b5b454bc1..4629ca7a2b 100644 --- a/src/com/android/launcher3/ExtendedEditText.java +++ b/src/com/android/launcher3/ExtendedEditText.java @@ -104,6 +104,7 @@ public class ExtendedEditText extends EditText { public void hideKeyboard() { hideKeyboardAsync(ActivityContext.lookupContext(getContext()), getWindowToken()); + clearFocus(); } private boolean showSoftInput() { diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java similarity index 90% rename from src/com/android/launcher3/BaseRecyclerView.java rename to src/com/android/launcher3/FastScrollRecyclerView.java index 9369bdc2fd..f117069144 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/FastScrollRecyclerView.java @@ -23,7 +23,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; -import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.compat.AccessibilityManagerCompat; @@ -37,19 +37,19 @@ import com.android.launcher3.views.RecyclerViewFastScroller; *

  • Enable fast scroller. * */ -public abstract class BaseRecyclerView extends RecyclerView { +public abstract class FastScrollRecyclerView extends RecyclerView { protected RecyclerViewFastScroller mScrollbar; - public BaseRecyclerView(Context context) { + public FastScrollRecyclerView(Context context) { this(context, null); } - public BaseRecyclerView(Context context, AttributeSet attrs) { + public FastScrollRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @@ -66,6 +66,7 @@ public abstract class BaseRecyclerView extends RecyclerView { onUpdateScrollbar(0); } + @Nullable public RecyclerViewFastScroller getScrollbar() { return mScrollbar; } @@ -197,13 +198,6 @@ public abstract class BaseRecyclerView extends RecyclerView { if (mScrollbar != null) { mScrollbar.reattachThumbToScroll(); } - if (getLayoutManager() instanceof LinearLayoutManager) { - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); - if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { - // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). - return; - } - } scrollToPosition(0); } -} \ No newline at end of file +} diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java index a199a5779a..fdf0101e66 100644 --- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java +++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java @@ -15,7 +15,7 @@ */ package com.android.launcher3; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java index 2a7e629247..c782dca4ca 100644 --- a/src/com/android/launcher3/GestureNavContract.java +++ b/src/com/android/launcher3/GestureNavContract.java @@ -18,20 +18,30 @@ package com.android.launcher3; import static android.content.Intent.EXTRA_COMPONENT_NAME; import static android.content.Intent.EXTRA_USER; +import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE; + import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Intent; import android.graphics.RectF; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Message; +import android.os.Messenger; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.view.SurfaceControl; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.views.ActivityContext; + +import java.lang.ref.WeakReference; + /** * Class to encapsulate the handshake protocol between Launcher and gestureNav. */ @@ -43,6 +53,7 @@ public class GestureNavContract { public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position"; public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control"; public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK"; + public static final String EXTRA_ON_FINISH_CALLBACK = "gesture_nav_contract_finish_callback"; public final ComponentName componentName; public final UserHandle user; @@ -59,10 +70,15 @@ public class GestureNavContract { * Sends the position information to the receiver */ @TargetApi(Build.VERSION_CODES.R) - public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) { + public void sendEndPosition(RectF position, ActivityContext context, + @Nullable SurfaceControl surfaceControl) { Bundle result = new Bundle(); result.putParcelable(EXTRA_ICON_POSITION, position); result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl); + if (sMessageReceiver == null) { + sMessageReceiver = new StaticMessageReceiver(); + } + result.putParcelable(EXTRA_ON_FINISH_CALLBACK, sMessageReceiver.setCurrentContext(context)); Message callback = Message.obtain(); callback.copyFrom(mCallback); @@ -98,4 +114,42 @@ public class GestureNavContract { } return null; } + + /** + * Message used for receiving gesture nav contract information. We use a static messenger to + * avoid leaking too make binders in case the receiving launcher does not handle the contract + * properly. + */ + private static StaticMessageReceiver sMessageReceiver = null; + + private static class StaticMessageReceiver implements Handler.Callback { + + private static final int MSG_CLOSE_LAST_TARGET = 0; + + private final Messenger mMessenger = + new Messenger(new Handler(Looper.getMainLooper(), this)); + + private WeakReference mLastTarget = new WeakReference<>(null); + + public Message setCurrentContext(ActivityContext context) { + mLastTarget = new WeakReference<>(context); + + Message msg = Message.obtain(); + msg.replyTo = mMessenger; + msg.what = MSG_CLOSE_LAST_TARGET; + return msg; + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (message.what == MSG_CLOSE_LAST_TARGET) { + ActivityContext lastContext = mLastTarget.get(); + if (lastContext != null) { + AbstractFloatingView.closeOpenViews(lastContext, false, TYPE_ICON_SURFACE); + } + return true; + } + return false; + } + } } diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index ffe38168ac..76106fc58d 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -41,7 +41,7 @@ public class Hotseat extends CellLayout implements Insettable { @ViewDebug.ExportedProperty(category = "launcher") private boolean mHasVerticalHotseat; - private Workspace mWorkspace; + private Workspace mWorkspace; private boolean mSendTouchToWorkspace; @Nullable private Consumer mOnVisibilityAggregatedCallback; @@ -84,6 +84,7 @@ public class Hotseat extends CellLayout implements Insettable { removeAllViewsInLayout(); mHasVerticalHotseat = hasVerticalHotseat; DeviceProfile dp = mActivity.getDeviceProfile(); + resetCellSize(dp); if (hasVerticalHotseat) { setGridSize(1, dp.numShownHotseatIcons); } else { @@ -110,10 +111,9 @@ public class Hotseat extends CellLayout implements Insettable { mQsb.setVisibility(View.VISIBLE); lp.gravity = Gravity.BOTTOM; lp.width = ViewGroup.LayoutParams.MATCH_PARENT; - lp.height = (grid.isTaskbarPresent + lp.height = grid.isTaskbarPresent ? grid.workspacePadding.bottom - : grid.hotseatBarSizePx) - + (grid.isTaskbarPresent ? grid.taskbarSize : insets.bottom); + : grid.hotseatBarSizePx + insets.bottom; } Rect padding = grid.getHotseatLayoutPadding(getContext()); @@ -122,7 +122,7 @@ public class Hotseat extends CellLayout implements Insettable { InsettableFrameLayout.dispatchInsets(this, insets); } - public void setWorkspace(Workspace w) { + public void setWorkspace(Workspace w) { mWorkspace = w; } @@ -173,8 +173,9 @@ public class Hotseat extends CellLayout implements Insettable { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int width = getShortcutsAndWidgets().getMeasuredWidth(); - mQsb.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + int qsbWidth = mActivity.getDeviceProfile().qsbWidth; + + mQsb.measure(MeasureSpec.makeMeasureSpec(qsbWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mQsbHeight, MeasureSpec.EXACTLY)); } @@ -183,7 +184,14 @@ public class Hotseat extends CellLayout implements Insettable { super.onLayout(changed, l, t, r, b); int qsbWidth = mQsb.getMeasuredWidth(); - int left = (r - l - qsbWidth) / 2; + int left; + if (mActivity.getDeviceProfile().isQsbInline) { + int qsbSpace = mActivity.getDeviceProfile().hotseatBorderSpace; + left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace + : l + getPaddingLeft() - qsbWidth - qsbSpace; + } else { + left = (r - l - qsbWidth) / 2; + } int right = left + qsbWidth; int bottom = b - t - mActivity.getDeviceProfile().getQsbOffsetY(); diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index ff7a90cb2c..db43b44f72 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -19,6 +19,7 @@ package com.android.launcher3; import static com.android.launcher3.Utilities.dpiFromPx; import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME; import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; +import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; @@ -54,11 +55,14 @@ import com.android.launcher3.util.IntArray; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Themes; import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.window.WindowManagerProxy; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -94,18 +98,18 @@ public class InvariantDeviceProfile { // Used for arrays to specify different sizes (e.g. border spaces, width/height) in different // constraints - static final int COUNT_SIZES = 5; + static final int COUNT_SIZES = 4; static final int INDEX_DEFAULT = 0; static final int INDEX_LANDSCAPE = 1; static final int INDEX_TWO_PANEL_PORTRAIT = 2; static final int INDEX_TWO_PANEL_LANDSCAPE = 3; - static final int INDEX_ALL_APPS = 4; /** * Number of icons per row and column in the workspace. */ public int numRows; public int numColumns; + public int numSearchContainerColumns; /** * Number of icons per row and column in the folder. @@ -122,15 +126,26 @@ public class InvariantDeviceProfile { public PointF[] borderSpaces; public float folderBorderSpace; + public float[] hotseatBorderSpaces; public float[] horizontalMargin; + public PointF[] allAppsCellSize; + public float[] allAppsIconSize; + public float[] allAppsIconTextSize; + public PointF[] allAppsBorderSpaces; + private SparseArray mExtraAttrs; /** * Number of icons inside the hotseat area. */ - protected int numShownHotseatIcons; + public int numShownHotseatIcons; + + /** + * Number of icons inside the hotseat area when using 3 buttons navigation. + */ + public int numShrunkenHotseatIcons; /** * Number of icons inside the hotseat area that is stored in the database. This is greater than @@ -139,6 +154,8 @@ public class InvariantDeviceProfile { */ public int numDatabaseHotseatIcons; + public int[] hotseatColumnSpan; + /** * Number of columns in the all apps list. */ @@ -154,6 +171,7 @@ public class InvariantDeviceProfile { public String dbFile; public int defaultLayoutId; int demoModeLayoutId; + boolean[] inlineQsb = new boolean[COUNT_SIZES]; /** * An immutable list of supported profiles. @@ -169,8 +187,7 @@ public class InvariantDeviceProfile { private final ArrayList mChangeListeners = new ArrayList<>(); @VisibleForTesting - public InvariantDeviceProfile() { - } + public InvariantDeviceProfile() { } @TargetApi(23) private InvariantDeviceProfile(Context context) { @@ -183,7 +200,8 @@ public class InvariantDeviceProfile { DisplayController.INSTANCE.get(context).setPriorityListener( (displayContext, info, flags) -> { - if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS)) != 0) { + if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS + | CHANGE_NAVIGATION_MODE)) != 0) { onConfigChanged(displayContext); } }); @@ -237,6 +255,8 @@ public class InvariantDeviceProfile { COUNT_SIZES); System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0, COUNT_SIZES); + System.arraycopy(defaultDisplayOption.inlineQsb, 0, result.inlineQsb, 0, + COUNT_SIZES); initGrid(context, myInfo, result, deviceType); } @@ -245,11 +265,12 @@ public class InvariantDeviceProfile { * Reinitialize the current grid after a restore, where some grids might now be disabled. */ public void reinitializeAfterRestore(Context context) { + String currentGridName = getCurrentGridName(context); String currentDbFile = dbFile; - String gridName = getCurrentGridName(context); - String newGridName = initGrid(context, gridName); - if (!newGridName.equals(gridName)) { - Log.d(TAG, "Restored grid is disabled : " + gridName + String newGridName = initGrid(context, currentGridName); + String newDbFile = dbFile; + if (!newDbFile.equals(currentDbFile)) { + Log.d(TAG, "Restored grid is disabled : " + currentGridName + ", migrating to: " + newGridName + ", removing all other grid db files"); for (String gridDbFile : LauncherFiles.GRID_DB_FILES) { @@ -260,16 +281,21 @@ public class InvariantDeviceProfile { Log.d(TAG, "Removed old grid db file: " + gridDbFile); } } - setCurrentGrid(context, gridName); + setCurrentGrid(context, newGridName); } } private static @DeviceType int getDeviceType(Info displayInfo) { - // Each screen has two profiles (portrait/landscape), so devices with four or more - // supported profiles implies two or more internal displays. - if (displayInfo.supportedBounds.size() >= 4 && ENABLE_TWO_PANEL_HOME.get()) { + int flagPhone = 1 << 0; + int flagTablet = 1 << 1; + + int type = displayInfo.supportedBounds.stream() + .mapToInt(bounds -> displayInfo.isTablet(bounds) ? flagTablet : flagPhone) + .reduce(0, (a, b) -> a | b); + if ((type == (flagPhone | flagTablet)) && ENABLE_TWO_PANEL_HOME.get()) { + // device has profiles supporting both phone and table modes return TYPE_MULTI_DISPLAY; - } else if (displayInfo.supportedBounds.stream().allMatch(displayInfo::isTablet)) { + } else if (type == flagTablet) { return TYPE_TABLET; } else { return TYPE_PHONE; @@ -300,6 +326,7 @@ public class InvariantDeviceProfile { GridOption closestProfile = displayOption.grid; numRows = closestProfile.numRows; numColumns = closestProfile.numColumns; + numSearchContainerColumns = closestProfile.numSearchContainerColumns; dbFile = closestProfile.dbFile; defaultLayoutId = closestProfile.defaultLayoutId; demoModeLayoutId = closestProfile.demoModeLayoutId; @@ -329,22 +356,31 @@ public class InvariantDeviceProfile { horizontalMargin = displayOption.horizontalMargin; numShownHotseatIcons = closestProfile.numHotseatIcons; + numShrunkenHotseatIcons = closestProfile.numShrunkenHotseatIcons; numDatabaseHotseatIcons = deviceType == TYPE_MULTI_DISPLAY ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons; + hotseatColumnSpan = closestProfile.hotseatColumnSpan; + hotseatBorderSpaces = displayOption.hotseatBorderSpaces; numAllAppsColumns = closestProfile.numAllAppsColumns; numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns; + allAppsCellSize = displayOption.allAppsCellSize; + allAppsBorderSpaces = displayOption.allAppsBorderSpaces; + allAppsIconSize = displayOption.allAppsIconSizes; + allAppsIconTextSize = displayOption.allAppsIconTextSizes; if (!Utilities.isGridOptionsEnabled(context)) { - iconSize[INDEX_ALL_APPS] = iconSize[INDEX_DEFAULT]; - iconTextSize[INDEX_ALL_APPS] = iconTextSize[INDEX_DEFAULT]; + allAppsIconSize = iconSize; + allAppsIconTextSize = iconTextSize; } if (devicePaddingId != 0) { devicePaddings = new DevicePaddings(context, devicePaddingId); } + inlineQsb = displayOption.inlineQsb; + // If the partner customization apk contains any grid overrides, apply them // Supported overrides: numRows, numColumns, iconSize applyPartnerDeviceProfileOverrides(context, metrics); @@ -354,7 +390,8 @@ public class InvariantDeviceProfile { for (WindowBounds bounds : displayInfo.supportedBounds) { localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo) .setUseTwoPanels(deviceType == TYPE_MULTI_DISPLAY) - .setWindowBounds(bounds).build()); + .setWindowBounds(bounds) + .build()); // Wallpaper size should be the maximum of the all possible sizes Launcher expects int displayWidth = bounds.bounds.width(); @@ -364,7 +401,8 @@ public class InvariantDeviceProfile { // We need to ensure that there is enough extra space in the wallpaper // for the intended parallax effects float parallaxFactor = - dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720 + dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.getDensityDpi()) + < 720 ? 2 : wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight); defaultWallpaperSize.x = @@ -393,8 +431,8 @@ public class InvariantDeviceProfile { private Object[] toModelState() { return new Object[]{ - numColumns, numRows, numDatabaseHotseatIcons, iconBitmapSize, fillResIconDpi, - numDatabaseAllAppsColumns, dbFile}; + numColumns, numRows, numSearchContainerColumns, numDatabaseHotseatIcons, + iconBitmapSize, fillResIconDpi, numDatabaseAllAppsColumns, dbFile}; } private void onConfigChanged(Context context) { @@ -553,8 +591,8 @@ public class InvariantDeviceProfile { } } - float width = dpiFromPx(minWidthPx, displayInfo.densityDpi); - float height = dpiFromPx(minHeightPx, displayInfo.densityDpi); + float width = dpiFromPx(minWidthPx, displayInfo.getDensityDpi()); + float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi()); // Sort the profiles based on the closeness to the device size Collections.sort(points, (a, b) -> @@ -593,10 +631,27 @@ public class InvariantDeviceProfile { float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density; float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density; - return getBestMatch(screenWidth, screenHeight); + int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context); + + if (Utilities.IS_DEBUG_DEVICE) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + DisplayController.INSTANCE.get(context).dump(printWriter); + printWriter.flush(); + Log.d("b/231312158", "getDeviceProfile -" + + "\nconfig: " + config + + "\ndisplayMetrics: " + res.getDisplayMetrics() + + "\nrotation: " + rotation + + "\n" + stringWriter.toString(), + new Exception()); + } + return getBestMatch(screenWidth, screenHeight, rotation); } - public DeviceProfile getBestMatch(float screenWidth, float screenHeight) { + /** + * Returns the device profile matching the provided screen configuration + */ + public DeviceProfile getBestMatch(float screenWidth, float screenHeight, int rotation) { DeviceProfile bestMatch = supportedProfiles.get(0); float minDiff = Float.MAX_VALUE; @@ -606,6 +661,8 @@ public class InvariantDeviceProfile { if (diff < minDiff) { minDiff = diff; bestMatch = profile; + } else if (diff == minDiff && profile.rotationHint == rotation) { + bestMatch = profile; } } return bestMatch; @@ -671,6 +728,7 @@ public class InvariantDeviceProfile { public final String name; public final int numRows; public final int numColumns; + public final int numSearchContainerColumns; public final boolean isEnabled; private final int numFolderRows; @@ -679,7 +737,9 @@ public class InvariantDeviceProfile { private final int numAllAppsColumns; private final int numDatabaseAllAppsColumns; private final int numHotseatIcons; + private final int numShrunkenHotseatIcons; private final int numDatabaseHotseatIcons; + private final int[] hotseatColumnSpan = new int[COUNT_SIZES]; private final String dbFile; @@ -697,6 +757,8 @@ public class InvariantDeviceProfile { name = a.getString(R.styleable.GridDisplayOption_name); numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0); numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0); + numSearchContainerColumns = a.getInt( + R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns); dbFile = a.getString(R.styleable.GridDisplayOption_dbFile); defaultLayoutId = a.getResourceId(deviceType == TYPE_MULTI_DISPLAY && a.hasValue( @@ -713,8 +775,20 @@ public class InvariantDeviceProfile { numHotseatIcons = a.getInt( R.styleable.GridDisplayOption_numHotseatIcons, numColumns); + numShrunkenHotseatIcons = a.getInt( + R.styleable.GridDisplayOption_numShrunkenHotseatIcons, numHotseatIcons / 2); numDatabaseHotseatIcons = a.getInt( R.styleable.GridDisplayOption_numExtendedHotseatIcons, 2 * numHotseatIcons); + hotseatColumnSpan[INDEX_DEFAULT] = a.getInt( + R.styleable.GridDisplayOption_hotseatColumnSpan, numColumns); + hotseatColumnSpan[INDEX_LANDSCAPE] = a.getInt( + R.styleable.GridDisplayOption_hotseatColumnSpanLandscape, numColumns); + hotseatColumnSpan[INDEX_TWO_PANEL_LANDSCAPE] = a.getInt( + R.styleable.GridDisplayOption_hotseatColumnSpanTwoPanelLandscape, + numColumns); + hotseatColumnSpan[INDEX_TWO_PANEL_PORTRAIT] = a.getInt( + R.styleable.GridDisplayOption_hotseatColumnSpanTwoPanelPortrait, + numColumns); numFolderRows = a.getInt( R.styleable.GridDisplayOption_numFolderRows, numRows); @@ -744,22 +818,35 @@ public class InvariantDeviceProfile { @VisibleForTesting static final class DisplayOption { + private static final int INLINE_QSB_FOR_PORTRAIT = 1 << 0; + private static final int INLINE_QSB_FOR_LANDSCAPE = 1 << 1; + private static final int INLINE_QSB_FOR_TWO_PANEL_PORTRAIT = 1 << 2; + private static final int INLINE_QSB_FOR_TWO_PANEL_LANDSCAPE = 1 << 3; + private static final int DONT_INLINE_QSB = 0; public final GridOption grid; private final float minWidthDps; private final float minHeightDps; private final boolean canBeDefault; + private final boolean[] inlineQsb = new boolean[COUNT_SIZES]; private final PointF[] minCellSize = new PointF[COUNT_SIZES]; private float folderBorderSpace; private final PointF[] borderSpaces = new PointF[COUNT_SIZES]; private final float[] horizontalMargin = new float[COUNT_SIZES]; + //TODO(http://b/228998082) remove this when 3 button spaces are fixed + private final float[] hotseatBorderSpaces = new float[COUNT_SIZES]; private final float[] iconSizes = new float[COUNT_SIZES]; private final float[] textSizes = new float[COUNT_SIZES]; + private final PointF[] allAppsCellSize = new PointF[COUNT_SIZES]; + private final float[] allAppsIconSizes = new float[COUNT_SIZES]; + private final float[] allAppsIconTextSizes = new float[COUNT_SIZES]; + private final PointF[] allAppsBorderSpaces = new PointF[COUNT_SIZES]; + DisplayOption(GridOption grid, Context context, AttributeSet attrs) { this.grid = grid; @@ -770,100 +857,212 @@ public class InvariantDeviceProfile { canBeDefault = a.getBoolean(R.styleable.ProfileDisplayOption_canBeDefault, false); + int inlineForRotation = a.getInt(R.styleable.ProfileDisplayOption_inlineQsb, + DONT_INLINE_QSB); + inlineQsb[INDEX_DEFAULT] = + (inlineForRotation & INLINE_QSB_FOR_PORTRAIT) == INLINE_QSB_FOR_PORTRAIT; + inlineQsb[INDEX_LANDSCAPE] = + (inlineForRotation & INLINE_QSB_FOR_LANDSCAPE) == INLINE_QSB_FOR_LANDSCAPE; + inlineQsb[INDEX_TWO_PANEL_PORTRAIT] = + (inlineForRotation & INLINE_QSB_FOR_TWO_PANEL_PORTRAIT) + == INLINE_QSB_FOR_TWO_PANEL_PORTRAIT; + inlineQsb[INDEX_TWO_PANEL_LANDSCAPE] = + (inlineForRotation & INLINE_QSB_FOR_TWO_PANEL_LANDSCAPE) + == INLINE_QSB_FOR_TWO_PANEL_LANDSCAPE; + float x; float y; - x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0); - y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0); + x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidth, 0); + y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeight, 0); minCellSize[INDEX_DEFAULT] = new PointF(x, y); - minCellSize[INDEX_LANDSCAPE] = new PointF(x, y); - minCellSize[INDEX_ALL_APPS] = new PointF(x, y); - x = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitMinCellWidthDps, + x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthLandscape, minCellSize[INDEX_DEFAULT].x); - y = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitMinCellHeightDps, + y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightLandscape, + minCellSize[INDEX_DEFAULT].y); + minCellSize[INDEX_LANDSCAPE] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthTwoPanelPortrait, + minCellSize[INDEX_DEFAULT].x); + y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightTwoPanelPortrait, minCellSize[INDEX_DEFAULT].y); minCellSize[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); - x = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeMinCellWidthDps, + x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthTwoPanelLandscape, minCellSize[INDEX_DEFAULT].x); - y = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeMinCellHeightDps, + y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightTwoPanelLandscape, minCellSize[INDEX_DEFAULT].y); minCellSize[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); - float borderSpace = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceDps, 0); - float twoPanelPortraitBorderSpaceDps = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceDps, borderSpace); - float twoPanelLandscapeBorderSpaceDps = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceDps, borderSpace); + float borderSpace = a.getFloat(R.styleable.ProfileDisplayOption_borderSpace, 0); + float borderSpaceLandscape = a.getFloat( + R.styleable.ProfileDisplayOption_borderSpaceLandscape, borderSpace); + float borderSpaceTwoPanelPortrait = a.getFloat( + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelPortrait, borderSpace); + float borderSpaceTwoPanelLandscape = a.getFloat( + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelLandscape, borderSpace); - x = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceHorizontalDps, borderSpace); - y = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceVerticalDps, borderSpace); + x = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceHorizontal, borderSpace); + y = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceVertical, borderSpace); borderSpaces[INDEX_DEFAULT] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceLandscapeHorizontal, + borderSpaceLandscape); + y = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceLandscapeVertical, + borderSpaceLandscape); borderSpaces[INDEX_LANDSCAPE] = new PointF(x, y); x = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceHorizontalDps, - twoPanelPortraitBorderSpaceDps); + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelPortraitHorizontal, + borderSpaceTwoPanelPortrait); y = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceVerticalDps, - twoPanelPortraitBorderSpaceDps); + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelPortraitVertical, + borderSpaceTwoPanelPortrait); borderSpaces[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); x = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceHorizontalDps, - twoPanelLandscapeBorderSpaceDps); + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelLandscapeHorizontal, + borderSpaceTwoPanelLandscape); y = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceVerticalDps, - twoPanelLandscapeBorderSpaceDps); + R.styleable.ProfileDisplayOption_borderSpaceTwoPanelLandscapeVertical, + borderSpaceTwoPanelLandscape); borderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); - x = y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellSpacingDps, - borderSpace); - borderSpaces[INDEX_ALL_APPS] = new PointF(x, y); folderBorderSpace = borderSpace; + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidth, + minCellSize[INDEX_DEFAULT].x); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeight, + minCellSize[INDEX_DEFAULT].y); + allAppsCellSize[INDEX_DEFAULT] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidthLandscape, + allAppsCellSize[INDEX_DEFAULT].x); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeightLandscape, + allAppsCellSize[INDEX_DEFAULT].y); + allAppsCellSize[INDEX_LANDSCAPE] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidthTwoPanelPortrait, + allAppsCellSize[INDEX_DEFAULT].x); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeightTwoPanelPortrait, + allAppsCellSize[INDEX_DEFAULT].y); + allAppsCellSize[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidthTwoPanelLandscape, + allAppsCellSize[INDEX_DEFAULT].x); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeightTwoPanelLandscape, + allAppsCellSize[INDEX_DEFAULT].y); + allAppsCellSize[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); + + float allAppsBorderSpace = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpace, borderSpace); + float allAppsBorderSpaceLandscape = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceLandscape, + allAppsBorderSpace); + float allAppsBorderSpaceTwoPanelPortrait = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelPortrait, + allAppsBorderSpace); + float allAppsBorderSpaceTwoPanelLandscape = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelLandscape, + allAppsBorderSpace); + + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsBorderSpaceHorizontal, + allAppsBorderSpace); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsBorderSpaceVertical, + allAppsBorderSpace); + allAppsBorderSpaces[INDEX_DEFAULT] = new PointF(x, y); + + x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsBorderSpaceLandscapeHorizontal, + allAppsBorderSpaceLandscape); + y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsBorderSpaceLandscapeVertical, + allAppsBorderSpaceLandscape); + allAppsBorderSpaces[INDEX_LANDSCAPE] = new PointF(x, y); + + x = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelPortraitHorizontal, + allAppsBorderSpaceTwoPanelPortrait); + y = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelPortraitVertical, + allAppsBorderSpaceTwoPanelPortrait); + allAppsBorderSpaces[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); + + x = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelLandscapeHorizontal, + allAppsBorderSpaceTwoPanelLandscape); + y = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsBorderSpaceTwoPanelLandscapeVertical, + allAppsBorderSpaceTwoPanelLandscape); + allAppsBorderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); + iconSizes[INDEX_DEFAULT] = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0); iconSizes[INDEX_LANDSCAPE] = - a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize, - iconSizes[INDEX_DEFAULT]); - iconSizes[INDEX_ALL_APPS] = - a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconSizeLandscape, iconSizes[INDEX_DEFAULT]); iconSizes[INDEX_TWO_PANEL_PORTRAIT] = - a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitIconSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconSizeTwoPanelPortrait, iconSizes[INDEX_DEFAULT]); iconSizes[INDEX_TWO_PANEL_LANDSCAPE] = - a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeIconSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconSizeTwoPanelLandscape, iconSizes[INDEX_DEFAULT]); + allAppsIconSizes[INDEX_DEFAULT] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconSize, iconSizes[INDEX_DEFAULT]); + allAppsIconSizes[INDEX_LANDSCAPE] = allAppsIconSizes[INDEX_DEFAULT]; + allAppsIconSizes[INDEX_TWO_PANEL_PORTRAIT] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconSizeTwoPanelPortrait, + allAppsIconSizes[INDEX_DEFAULT]); + allAppsIconSizes[INDEX_TWO_PANEL_LANDSCAPE] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconSizeTwoPanelLandscape, + allAppsIconSizes[INDEX_DEFAULT]); + textSizes[INDEX_DEFAULT] = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0); textSizes[INDEX_LANDSCAPE] = - a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconTextSize, - textSizes[INDEX_DEFAULT]); - textSizes[INDEX_ALL_APPS] = - a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconTextSizeLandscape, textSizes[INDEX_DEFAULT]); textSizes[INDEX_TWO_PANEL_PORTRAIT] = - a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitIconTextSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconTextSizeTwoPanelPortrait, textSizes[INDEX_DEFAULT]); textSizes[INDEX_TWO_PANEL_LANDSCAPE] = - a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeIconTextSize, + a.getFloat(R.styleable.ProfileDisplayOption_iconTextSizeTwoPanelLandscape, textSizes[INDEX_DEFAULT]); + allAppsIconTextSizes[INDEX_DEFAULT] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconTextSize, textSizes[INDEX_DEFAULT]); + allAppsIconTextSizes[INDEX_LANDSCAPE] = allAppsIconTextSizes[INDEX_DEFAULT]; + allAppsIconTextSizes[INDEX_TWO_PANEL_PORTRAIT] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconTextSizeTwoPanelPortrait, + allAppsIconTextSizes[INDEX_DEFAULT]); + allAppsIconTextSizes[INDEX_TWO_PANEL_LANDSCAPE] = a.getFloat( + R.styleable.ProfileDisplayOption_allAppsIconTextSizeTwoPanelLandscape, + allAppsIconTextSizes[INDEX_DEFAULT]); + horizontalMargin[INDEX_DEFAULT] = a.getFloat( R.styleable.ProfileDisplayOption_horizontalMargin, 0); - horizontalMargin[INDEX_LANDSCAPE] = horizontalMargin[INDEX_DEFAULT]; - horizontalMargin[INDEX_ALL_APPS] = horizontalMargin[INDEX_DEFAULT]; + horizontalMargin[INDEX_LANDSCAPE] = a.getFloat( + R.styleable.ProfileDisplayOption_horizontalMarginLandscape, + horizontalMargin[INDEX_DEFAULT]); horizontalMargin[INDEX_TWO_PANEL_LANDSCAPE] = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelLandscapeHorizontalMargin, + R.styleable.ProfileDisplayOption_horizontalMarginTwoPanelLandscape, horizontalMargin[INDEX_DEFAULT]); horizontalMargin[INDEX_TWO_PANEL_PORTRAIT] = a.getFloat( - R.styleable.ProfileDisplayOption_twoPanelPortraitHorizontalMargin, + R.styleable.ProfileDisplayOption_horizontalMarginTwoPanelPortrait, horizontalMargin[INDEX_DEFAULT]); + hotseatBorderSpaces[INDEX_DEFAULT] = a.getFloat( + R.styleable.ProfileDisplayOption_hotseatBorderSpace, borderSpace); + hotseatBorderSpaces[INDEX_LANDSCAPE] = a.getFloat( + R.styleable.ProfileDisplayOption_hotseatBorderSpaceLandscape, + hotseatBorderSpaces[INDEX_DEFAULT]); + hotseatBorderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = a.getFloat( + R.styleable.ProfileDisplayOption_hotseatBorderSpaceTwoPanelLandscape, + hotseatBorderSpaces[INDEX_DEFAULT]); + hotseatBorderSpaces[INDEX_TWO_PANEL_PORTRAIT] = a.getFloat( + R.styleable.ProfileDisplayOption_hotseatBorderSpaceTwoPanelPortrait, + hotseatBorderSpaces[INDEX_DEFAULT]); + a.recycle(); } @@ -881,6 +1080,11 @@ public class InvariantDeviceProfile { textSizes[i] = 0; borderSpaces[i] = new PointF(); minCellSize[i] = new PointF(); + allAppsCellSize[i] = new PointF(); + allAppsIconSizes[i] = 0; + allAppsIconTextSizes[i] = 0; + allAppsBorderSpaces[i] = new PointF(); + inlineQsb[i] = false; } } @@ -893,6 +1097,13 @@ public class InvariantDeviceProfile { minCellSize[i].x *= w; minCellSize[i].y *= w; horizontalMargin[i] *= w; + hotseatBorderSpaces[i] *= w; + allAppsCellSize[i].x *= w; + allAppsCellSize[i].y *= w; + allAppsIconSizes[i] *= w; + allAppsIconTextSizes[i] *= w; + allAppsBorderSpaces[i].x *= w; + allAppsBorderSpaces[i].y *= w; } folderBorderSpace *= w; @@ -909,6 +1120,14 @@ public class InvariantDeviceProfile { minCellSize[i].x += p.minCellSize[i].x; minCellSize[i].y += p.minCellSize[i].y; horizontalMargin[i] += p.horizontalMargin[i]; + hotseatBorderSpaces[i] += p.hotseatBorderSpaces[i]; + allAppsCellSize[i].x += p.allAppsCellSize[i].x; + allAppsCellSize[i].y += p.allAppsCellSize[i].y; + allAppsIconSizes[i] += p.allAppsIconSizes[i]; + allAppsIconTextSizes[i] += p.allAppsIconTextSizes[i]; + allAppsBorderSpaces[i].x += p.allAppsBorderSpaces[i].x; + allAppsBorderSpaces[i].y += p.allAppsBorderSpaces[i].y; + inlineQsb[i] |= p.inlineQsb[i]; } folderBorderSpace += p.folderBorderSpace; diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 87eb222c62..ebed31bd54 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; @@ -40,8 +42,7 @@ import static com.android.launcher3.LauncherState.NO_SCALE; import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions; -import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; -import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD; +import static com.android.launcher3.logging.StatsLogManager.EventEnum; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY; @@ -63,7 +64,6 @@ import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Notification; @@ -107,6 +107,7 @@ import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.animation.OvershootInterpolator; @@ -120,13 +121,14 @@ import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.accessibility.BaseAccessibilityDelegate.LauncherAction; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; -import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.LauncherAction; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.allapps.AllAppsRecyclerView; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.BaseAllAppsContainerView; import com.android.launcher3.allapps.DiscoveryBounce; -import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.PropertyListBuilder; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; @@ -142,6 +144,8 @@ import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.icons.IconCache; import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.logger.LauncherAtom; +import com.android.launcher3.logger.LauncherAtom.ContainerInfo; +import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer; import com.android.launcher3.logging.FileLog; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; @@ -150,6 +154,7 @@ import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.model.ModelUtils; import com.android.launcher3.model.ModelWriter; +import com.android.launcher3.model.StringCache; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; @@ -178,9 +183,6 @@ import com.android.launcher3.util.ActivityTracker; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; -import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; @@ -195,6 +197,7 @@ import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.FloatingIconView; import com.android.launcher3.views.FloatingSurfaceView; import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.views.ScrimView; @@ -209,7 +212,7 @@ import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.custom.CustomWidgetManager; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; -import com.android.systemui.plugins.OverlayPlugin; +import com.android.systemui.plugins.LauncherOverlayPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.shared.LauncherExterns; import com.android.systemui.plugins.shared.LauncherOverlayManager; @@ -224,6 +227,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -231,9 +235,9 @@ import java.util.stream.Stream; /** * Default launcher application. */ -public class Launcher extends StatefulActivity implements LauncherExterns, - Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener, - LauncherOverlayCallbacks { +public class Launcher extends StatefulActivity + implements LauncherExterns, Callbacks, InvariantDeviceProfile.OnIDPChangeListener, + PluginListener, LauncherOverlayCallbacks { public static final String TAG = "Launcher"; public static final ActivityTracker ACTIVITY_TRACKER = new ActivityTracker<>(); @@ -298,7 +302,7 @@ public class Launcher extends StatefulActivity implements Launche private Configuration mOldConfig; @Thunk - Workspace mWorkspace; + Workspace mWorkspace; @Thunk DragLayer mDragLayer; private DragController mDragController; @@ -315,7 +319,7 @@ public class Launcher extends StatefulActivity implements Launche // Main container view for the all apps screen. @Thunk - AllAppsContainerView mAppsView; + ActivityAllAppsContainerView mAppsView; AllAppsTransitionController mAllAppsController; // Scrim view for the all apps and overview state. @@ -333,6 +337,7 @@ public class Launcher extends StatefulActivity implements Launche private Runnable mOnDeferredActivityLaunchCallback; private ViewOnDrawExecutor mPendingExecutor; + private OnPreDrawListener mOnInitialBindListener; private LauncherModel mModel; private ModelWriter mModelWriter; @@ -347,7 +352,7 @@ public class Launcher extends StatefulActivity implements Launche // We only want to get the SharedPreferences once since it does an FS stat each time we get // it from the context. private SharedPreferences mSharedPrefs; - private OnboardingPrefs mOnboardingPrefs; + private OnboardingPrefs mOnboardingPrefs; // Activity result which needs to be processed after workspace has loaded. private ActivityResultInfo mPendingActivityResult; @@ -380,6 +385,8 @@ public class Launcher extends StatefulActivity implements Launche protected InstanceId mAllAppsSessionLogId; private LauncherState mPrevLauncherState; + private StringCache mStringCache; + @Override @TargetApi(Build.VERSION_CODES.S) protected void onCreate(Bundle savedInstanceState) { @@ -426,8 +433,7 @@ public class Launcher extends StatefulActivity implements Launche shareIntent.putExtra(Intent.EXTRA_TEXT, stackTrace); shareIntent = Intent.createChooser(shareIntent, null); PendingIntent sharePendingIntent = PendingIntent.getActivity( - this, 0, shareIntent, PendingIntent.FLAG_UPDATE_CURRENT - ); + this, 0, shareIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE); Notification notification = new Notification.Builder(this, notificationChannelId) .setSmallIcon(android.R.drawable.ic_menu_close_clear_cancel) @@ -494,11 +500,10 @@ public class Launcher extends StatefulActivity implements Launche if (!mModel.addCallbacksAndLoad(this)) { if (!internalStateHandled) { - Log.d(BAD_STATE, "Launcher onCreate not binding sync, setting DragLayer alpha " - + "ALPHA_INDEX_LAUNCHER_LOAD to 0"); - // If we are not binding synchronously, show a fade in animation when - // the first page bind completes. - mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0); + Log.d(BAD_STATE, "Launcher onCreate not binding sync, prevent drawing"); + // If we are not binding synchronously, pause drawing until initial bind complete, + // so that the system could continue to show the device loading prompt + mOnInitialBindListener = Boolean.FALSE::booleanValue; } } @@ -506,6 +511,9 @@ public class Launcher extends StatefulActivity implements Launche setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setContentView(getRootView()); + if (mOnInitialBindListener != null) { + getRootView().getViewTreeObserver().addOnPreDrawListener(mOnInitialBindListener); + } getRootView().dispatchInsets(); // Listen for broadcasts @@ -519,7 +527,7 @@ public class Launcher extends StatefulActivity implements Launche } mOverlayManager = getDefaultOverlay(); PluginManagerWrapper.INSTANCE.get(this).addPluginListener(this, - OverlayPlugin.class, false /* allowedMultiple */); + LauncherOverlayPlugin.class, false /* allowedMultiple */); mRotationHelper.initialize(); TraceHelper.INSTANCE.endSection(traceToken); @@ -530,27 +538,29 @@ public class Launcher extends StatefulActivity implements Launche if (Utilities.ATLEAST_R) { getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING); } + setTitle(R.string.home_screen); } protected LauncherOverlayManager getDefaultOverlay() { return new LauncherOverlayManager() { }; } - protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) { + protected OnboardingPrefs createOnboardingPrefs( + SharedPreferences sharedPrefs) { return new OnboardingPrefs<>(this, sharedPrefs); } - public OnboardingPrefs getOnboardingPrefs() { + public OnboardingPrefs getOnboardingPrefs() { return mOnboardingPrefs; } @Override - public void onPluginConnected(OverlayPlugin overlayManager, Context context) { + public void onPluginConnected(LauncherOverlayPlugin overlayManager, Context context) { switchOverlay(() -> overlayManager.createOverlayManager(this, this)); } @Override - public void onPluginDisconnected(OverlayPlugin plugin) { + public void onPluginDisconnected(LauncherOverlayPlugin plugin) { switchOverlay(this::getDefaultOverlay); } @@ -567,7 +577,7 @@ public class Launcher extends StatefulActivity implements Launche } @Override - protected void dispatchDeviceProfileChanged() { + public void dispatchDeviceProfileChanged() { super.dispatchDeviceProfileChanged(); mOverlayManager.onDeviceProvideChanged(); } @@ -576,7 +586,13 @@ public class Launcher extends StatefulActivity implements Launche public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE); - AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE); + // Starting with Android S, onEnterAnimationComplete is sent immediately + // causing the surface to get removed before the animation completed (b/175345344). + // Instead we rely on next user touch event to remove the view and optionally a callback + // from system from Android T onwards. + if (!Utilities.ATLEAST_S) { + AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE); + } } @Override @@ -623,7 +639,7 @@ public class Launcher extends StatefulActivity implements Launche mDragLayer.onOneHandedModeStateChanged(activated); } - private void initDeviceProfile(InvariantDeviceProfile idp) { + protected void initDeviceProfile(InvariantDeviceProfile idp) { // Load configuration-specific DeviceProfile mDeviceProfile = idp.getDeviceProfile(this); if (isInMultiWindowMode()) { @@ -677,6 +693,8 @@ public class Launcher extends StatefulActivity implements Launche return !isWorkspaceLoading(); } + @NonNull + @Override public PopupDataProvider getPopupDataProvider() { return mPopupDataProvider; } @@ -956,7 +974,7 @@ public class Launcher extends StatefulActivity implements Launche hideKeyboard(); logStopAndResume(false /* isResume */); mAppWidgetHost.setActivityStarted(false); - NotificationListener.removeNotificationsChangedListener(); + NotificationListener.removeNotificationsChangedListener(getPopupDataProvider()); } @Override @@ -985,13 +1003,10 @@ public class Launcher extends StatefulActivity implements Launche mModel.validateModelDataOnResume(); // Set the notification listener and fetch updated notifications when we resume - NotificationListener.setNotificationsChangedListener(mPopupDataProvider); + NotificationListener.addNotificationsChangedListener(mPopupDataProvider); DiscoveryBounce.showForHomeIfNeeded(this); mAppWidgetHost.setActivityResumed(true); - - // Temporary workaround for apps using SHOW_FORCED IME flag. - hideKeyboard(); } private void logStopAndResume(boolean isResume) { @@ -1087,14 +1102,25 @@ public class Launcher extends StatefulActivity implements Launche && mAllAppsSessionLogId == null) { // creates new instance ID since new all apps session is started. mAllAppsSessionLogId = new InstanceIdSequence().newInstanceId(); - getStatsLogManager() - .logger() - .log(FeatureFlags.ENABLE_DEVICE_SEARCH.get() - ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH - : LAUNCHER_ALLAPPS_ENTRY); + if (getAllAppsEntryEvent().isPresent()) { + getStatsLogManager().logger() + .withContainerInfo(ContainerInfo.newBuilder() + .setWorkspace(WorkspaceContainer.newBuilder() + .setPageIndex(getWorkspace().getCurrentPage())).build()) + .log(getAllAppsEntryEvent().get()); + } } } + /** + * Returns {@link EventEnum} that should be logged when Launcher enters into AllApps state. + */ + protected Optional getAllAppsEntryEvent() { + return Optional.of(FeatureFlags.ENABLE_DEVICE_SEARCH.get() + ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH + : LAUNCHER_ALLAPPS_ENTRY); + } + @Override public void onStateSetEnd(LauncherState state) { super.onStateSetEnd(state); @@ -1121,17 +1147,18 @@ public class Launcher extends StatefulActivity implements Launche // Making sure mAllAppsSessionLogId is not null to avoid double logging. && mAllAppsSessionLogId != null) { getAppsView().reset(false); - getStatsLogManager().logger() - .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder() - .setWorkspace( - LauncherAtom.WorkspaceContainer.newBuilder() - .setPageIndex(getWorkspace().getCurrentPage())) - .build()) - .log(LAUNCHER_ALLAPPS_EXIT); + getAllAppsExitEvent().ifPresent(getStatsLogManager().logger()::log); mAllAppsSessionLogId = null; } } + /** + * Returns {@link EventEnum} that should be logged when Launcher exists from AllApps state. + */ + protected Optional getAllAppsExitEvent() { + return Optional.of(LAUNCHER_ALLAPPS_EXIT); + } + @Override protected void onResume() { Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT, @@ -1144,6 +1171,8 @@ public class Launcher extends StatefulActivity implements Launche mOverlayManager.onActivityResumed(this); } + AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE); + DragView.removeAllViews(this); TraceHelper.INSTANCE.endSection(traceToken); } @@ -1254,13 +1283,16 @@ public class Launcher extends StatefulActivity implements Launche * @param info The data structure describing the shortcut. */ View createShortcut(WorkspaceItemInfo info) { - return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + // This can be called before PagedView#pageScrollsInitialized returns true, so use the + // first page, which we always assume to be present. + return createShortcut((ViewGroup) mWorkspace.getChildAt(0), info); } /** * Creates a view representing a shortcut inflated from the specified resource. * - * @param parent The group the shortcut belongs to. + * @param parent The group the shortcut belongs to. This is not necessarily the group where + * the shortcut should be added. * @param info The data structure describing the shortcut. * @return A View inflated from layoutResId. */ @@ -1476,11 +1508,12 @@ public class Launcher extends StatefulActivity implements Launche return mDragLayer; } - public AllAppsContainerView getAppsView() { + @Override + public ActivityAllAppsContainerView getAppsView() { return mAppsView; } - public Workspace getWorkspace() { + public Workspace getWorkspace() { return mWorkspace; } @@ -1550,6 +1583,7 @@ public class Launcher extends StatefulActivity implements Launche boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction()); boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this); hideKeyboard(); + if (isActionMain) { if (!internalStateHandled) { // In all these cases, only animate if we're already on home @@ -1578,6 +1612,8 @@ public class Launcher extends StatefulActivity implements Launche handleGestureContract(intent); } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) { showAllAppsFromIntent(alreadyOnHome); + } else if (Intent.ACTION_SHOW_WORK_APPS.equals(intent.getAction())) { + showAllAppsWorkTabFromIntent(alreadyOnHome); } TraceHelper.INSTANCE.endSection(traceToken); @@ -1588,6 +1624,11 @@ public class Launcher extends StatefulActivity implements Launche getStateManager().goToState(ALL_APPS, alreadyOnHome); } + private void showAllAppsWorkTabFromIntent(boolean alreadyOnHome) { + showAllAppsFromIntent(alreadyOnHome); + mAppsView.switchToTab(BaseAllAppsContainerView.AdapterHolder.WORK); + } + /** * Handles gesture nav contract */ @@ -1638,13 +1679,8 @@ public class Launcher extends StatefulActivity implements Launche outState.remove(RUNTIME_STATE_WIDGET_PANEL); } - // We close any open folders and shortcut containers that are not safe for rebind, - // and we need to make sure this state is reflected. - AbstractFloatingView.closeOpenViews(this, false, TYPE_ALL & ~TYPE_REBIND_SAFE); finishAutoCancelActionMode(); - DragView.removeAllViews(this); - if (mPendingRequestArgs != null) { outState.putParcelable(RUNTIME_STATE_PENDING_REQUEST_ARGS, mPendingRequestArgs); } @@ -1744,6 +1780,11 @@ public class Launcher extends StatefulActivity implements Launche return mWorkspaceLoading; } + @Override + public boolean isBindingItems() { + return mWorkspaceLoading; + } + private void setWorkspaceLoading(boolean value) { mWorkspaceLoading = value; } @@ -1918,6 +1959,19 @@ public class Launcher extends StatefulActivity implements Launche * @param deleteFromDb whether or not to delete this item from the db. */ public boolean removeItem(View v, final ItemInfo itemInfo, boolean deleteFromDb) { + return removeItem(v, itemInfo, deleteFromDb, null); + } + + /** + * Unbinds the view for the specified item, and removes the item and all its children. + * + * @param v the view being removed. + * @param itemInfo the {@link ItemInfo} for this view. + * @param deleteFromDb whether or not to delete this item from the db. + * @param reason the resaon for removal. + */ + public boolean removeItem(View v, final ItemInfo itemInfo, boolean deleteFromDb, + @Nullable final String reason) { if (itemInfo instanceof WorkspaceItemInfo) { // Remove the shortcut from the folder before removing it from launcher View folderIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container); @@ -1927,7 +1981,7 @@ public class Launcher extends StatefulActivity implements Launche mWorkspace.removeWorkspaceItem(v); } if (deleteFromDb) { - getModelWriter().deleteItemFromDatabase(itemInfo); + getModelWriter().deleteItemFromDatabase(itemInfo, reason); } } else if (itemInfo instanceof FolderInfo) { final FolderInfo folderInfo = (FolderInfo) itemInfo; @@ -1942,7 +1996,7 @@ public class Launcher extends StatefulActivity implements Launche final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo; mWorkspace.removeWorkspaceItem(v); if (deleteFromDb) { - getModelWriter().deleteWidgetInfo(widgetInfo, getAppWidgetHost()); + getModelWriter().deleteWidgetInfo(widgetInfo, getAppWidgetHost(), reason); } } else { return false; @@ -1994,13 +2048,16 @@ public class Launcher extends StatefulActivity implements Launche // Note: There should be at most one log per method call. This is enforced implicitly // by using if-else statements. AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this); - if (topView != null && topView.onBackPressed()) { - // Handled by the floating view. - } else { - mStateManager.getState().onBackPressed(this); + if (topView == null || !topView.onBackPressed()) { + // Not handled by the floating view. + onStateBack(); } } + protected void onStateBack() { + mStateManager.getState().onBackPressed(this); + } + protected void onScreenOff() { // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop @@ -2014,7 +2071,7 @@ public class Launcher extends StatefulActivity implements Launche @TargetApi(Build.VERSION_CODES.M) @Override - protected boolean onErrorStartingShortcut(Intent intent, ItemInfo info) { + public boolean onErrorStartingShortcut(Intent intent, ItemInfo info) { // Due to legacy reasons, direct call shortcuts require Launchers to have the // corresponding permission. Show the appropriate permission prompt if that // is the case. @@ -2305,7 +2362,7 @@ public class Launcher extends StatefulActivity implements Launche // Get the list of added items and intersect them with the set of items here final Collection bounceAnims = new ArrayList<>(); boolean canAnimatePageChange = canAnimatePageChange(); - Workspace workspace = mWorkspace; + Workspace workspace = mWorkspace; int newItemsScreenId = -1; int end = items.size(); View newView = null; @@ -2352,14 +2409,20 @@ public class Launcher extends StatefulActivity implements Launche CellLayout cl = mWorkspace.getScreenWithId(item.screenId); if (cl != null && cl.isOccupied(item.cellX, item.cellY)) { View v = cl.getChildAt(item.cellX, item.cellY); + if (v == null) { + Log.e(TAG, "bindItems failed when removing colliding item=" + item); + } Object tag = v.getTag(); String desc = "Collision while binding workspace item: " + item + ". Collides with " + tag; if (FeatureFlags.IS_STUDIO_BUILD) { throw (new RuntimeException(desc)); } else { - Log.d(TAG, desc); - getModelWriter().deleteItemFromDatabase(item); + getModelWriter().deleteItemFromDatabase(item, desc); + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + TAG + "bindItems failed for item=" + item); + } continue; } } @@ -2435,7 +2498,8 @@ public class Launcher extends StatefulActivity implements Launche if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) { item.providerName = QsbContainerView.getSearchComponentName(this); if (item.providerName == null) { - getModelWriter().deleteItemFromDatabase(item); + getModelWriter().deleteItemFromDatabase(item, + "search widget removed because search component cannot be found"); return null; } } @@ -2486,10 +2550,10 @@ public class Launcher extends StatefulActivity implements Launche if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) { if (appWidgetInfo == null) { - FileLog.d(TAG, "Removing restored widget: id=" + item.appWidgetId + getModelWriter().deleteItemFromDatabase(item, + "Removing restored widget: id=" + item.appWidgetId + " belongs to component " + item.providerName + " user " + item.user + ", as the provider is null and " + removalReason); - getModelWriter().deleteItemFromDatabase(item); return null; } @@ -2559,7 +2623,7 @@ public class Launcher extends StatefulActivity implements Launche // Verify that we own the widget if (appWidgetInfo == null) { FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId); - getModelWriter().deleteWidgetInfo(item, getAppWidgetHost()); + getModelWriter().deleteWidgetInfo(item, getAppWidgetHost(), removalReason); return null; } @@ -2629,36 +2693,12 @@ public class Launcher extends StatefulActivity implements Launche AllAppsStore.DEFER_UPDATES_NEXT_DRAW)); } - AlphaProperty property = mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD); - if (property.getValue() < 1) { - ObjectAnimator anim = ObjectAnimator.ofFloat(property, MultiValueAlpha.VALUE, 1); - - Log.d(BAD_STATE, "Launcher onInitialBindComplete toAlpha=" + 1); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - Log.d(BAD_STATE, "Launcher onInitialBindComplete onStart"); - } - - @Override - public void onAnimationCancel(Animator animation) { - float alpha = mDragLayer == null - ? -1 - : mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).getValue(); - Log.d(BAD_STATE, "Launcher onInitialBindComplete onCancel, alpha=" + alpha); - } - - @Override - public void onAnimationEnd(Animator animation) { - Log.d(BAD_STATE, "Launcher onInitialBindComplete onEnd"); - } - }); - - anim.addListener(AnimatorListeners.forEndCallback(executor::onLoadAnimationCompleted)); - anim.start(); - } else { - executor.onLoadAnimationCompleted(); + if (mOnInitialBindListener != null) { + getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener); + mOnInitialBindListener = null; } + + executor.onLoadAnimationCompleted(); executor.attachTo(this); if (Utilities.ATLEAST_S) { Trace.endAsyncSection(DISPLAY_WORKSPACE_TRACE_METHOD_NAME, @@ -2719,11 +2759,11 @@ public class Launcher extends StatefulActivity implements Launche * @param supportsAllAppsState If true and we are in All Apps state, looks for view in All Apps. * Else we only looks on the workspace. */ - public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user, - boolean supportsAllAppsState) { - final ItemInfoMatcher preferredItem = (info, cn) -> + public @Nullable View getFirstMatchForAppClose(int preferredItemId, String packageName, + UserHandle user, boolean supportsAllAppsState) { + final Predicate preferredItem = info -> info != null && info.id == preferredItemId; - final ItemInfoMatcher packageAndUserAndApp = (info, cn) -> + final Predicate packageAndUserAndApp = info -> info != null && info.itemType == ITEM_TYPE_APPLICATION && info.user.equals(user) @@ -2732,8 +2772,21 @@ public class Launcher extends StatefulActivity implements Launche packageName); if (supportsAllAppsState && isInState(LauncherState.ALL_APPS)) { - return getFirstMatch(Collections.singletonList(mAppsView.getActiveRecyclerView()), + AllAppsRecyclerView activeRecyclerView = mAppsView.getActiveRecyclerView(); + View v = getFirstMatch(Collections.singletonList(activeRecyclerView), preferredItem, packageAndUserAndApp); + + if (v != null && activeRecyclerView.getCurrentScrollY() > 0) { + RectF locationBounds = new RectF(); + FloatingIconView.getLocationBoundsForView(this, v, false, locationBounds, + new Rect()); + if (locationBounds.top < mAppsView.getHeaderBottom()) { + // Icon is covered by scrim, return null to play fallback animation. + return null; + } + } + + return v; } else { List containers = new ArrayList<>(mWorkspace.getPanelCount() + 1); containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets()); @@ -2741,14 +2794,8 @@ public class Launcher extends StatefulActivity implements Launche -> containers.add(((CellLayout) page).getShortcutsAndWidgets())); // Order: Preferred item by itself or in folder, then by matching package/user - if (ADAPTIVE_ICON_WINDOW_ANIM.get()) { - return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem), - packageAndUserAndApp, forFolderMatch(packageAndUserAndApp)); - } else { - // Do not use Folder as a criteria, since it'll cause a crash when trying to draw - // FolderAdaptiveIcon as the background. - return getFirstMatch(containers, preferredItem, packageAndUserAndApp); - } + return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem), + packageAndUserAndApp, forFolderMatch(packageAndUserAndApp)); } } @@ -2757,9 +2804,10 @@ public class Launcher extends StatefulActivity implements Launche * @param containers List of ViewGroups to scan, in order of preference. * @param operators List of operators, in order starting from best matching operator. */ + @Nullable private static View getFirstMatch(Iterable containers, - final ItemInfoMatcher... operators) { - for (ItemInfoMatcher operator : operators) { + final Predicate... operators) { + for (Predicate operator : operators) { for (ViewGroup container : containers) { View match = mapOverViewGroup(container, operator); if (match != null) { @@ -2774,11 +2822,12 @@ public class Launcher extends StatefulActivity implements Launche * Returns the first view matching the operator in the given ViewGroups, or null if none. * Forward iteration matters. */ - private static View mapOverViewGroup(ViewGroup container, ItemInfoMatcher op) { + @Nullable + private static View mapOverViewGroup(ViewGroup container, Predicate op) { final int itemCount = container.getChildCount(); for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { View item = container.getChildAt(itemIdx); - if (op.matchesInfo((ItemInfo) item.getTag())) { + if (op.test((ItemInfo) item.getTag())) { return item; } } @@ -2797,6 +2846,17 @@ public class Launcher extends StatefulActivity implements Launche getDragLayer().announceForAccessibility(getString(stringResId)); } + /** + * Informs us that the overlay (-1 screen, typically), has either become visible or invisible. + */ + public void onOverlayVisibilityChanged(boolean visible) {} + + /** + * Informs us that the page transition has ended, so that we can react to the newly selected + * page if we want to. + */ + public void onPageEndTransition() {} + /** * Add the icons for all apps. * @@ -2864,7 +2924,7 @@ public class Launcher extends StatefulActivity implements Launche * package-removal should clear all items by package name. */ @Override - public void bindWorkspaceComponentsRemoved(final ItemInfoMatcher matcher) { + public void bindWorkspaceComponentsRemoved(Predicate matcher) { mWorkspace.removeItemsByMatcher(matcher); mDragController.onAppsRemoved(matcher); PopupContainerWithArrow.dismissInvalidPopup(this); @@ -2875,6 +2935,16 @@ public class Launcher extends StatefulActivity implements Launche mPopupDataProvider.setAllWidgets(allWidgets); } + @Override + public void bindStringCache(StringCache cache) { + mStringCache = cache; + } + + @Override + public StringCache getStringCache() { + return mStringCache; + } + /** * @param packageUser if null, refreshes all widgets and shortcuts, otherwise only * refreshes the widgets and shortcuts associated with the given package/user @@ -3135,6 +3205,24 @@ public class Launcher extends StatefulActivity implements Launche return new DragOptions(); } + /** + * Animates Launcher elements during a transition to the All Apps page. + * + * @param progress Transition progress from 0 to 1; where 0 => home and 1 => all apps. + */ + public void onAllAppsTransition(float progress) { + // No-Op + } + + /** + * Animates Launcher elements during a transition to the Widgets pages. + * + * @param progress Transition progress from 0 to 1; where 0 => home and 1 => widgets. + */ + public void onWidgetsTransition(float progress) { + // No-Op + } + private static class NonConfigInstance { public Configuration config; public Bitmap snapshot; @@ -3153,4 +3241,29 @@ public class Launcher extends StatefulActivity implements Launche public ArrowPopup getOptionsPopup() { return findViewById(R.id.popup_container); } + + /** Pauses view updates that should not be run during the app launch animation. */ + public void pauseExpensiveViewUpdates() { + // Pause page indicator animations as they lead to layer trashing. + getWorkspace().getPageIndicator().pauseAnimations(); + + getWorkspace().mapOverItems((info, view) -> { + if (view instanceof LauncherAppWidgetHostView) { + ((LauncherAppWidgetHostView) view).beginDeferringUpdates(); + } + return false; // Return false to continue iterating through all the items. + }); + } + + /** Resumes view updates at the end of the app launch animation. */ + public void resumeExpensiveViewUpdates() { + getWorkspace().getPageIndicator().skipAnimationsToEnd(); + + getWorkspace().mapOverItems((info, view) -> { + if (view instanceof LauncherAppWidgetHostView) { + ((LauncherAppWidgetHostView) view).endDeferringUpdates(); + } + return false; // Return false to continue iterating through all the items. + }); + } } diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index b56c0127ff..808bf96f9f 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -27,6 +27,8 @@ import android.util.IntProperty; import android.view.View; import android.view.ViewGroup.LayoutParams; +import com.android.launcher3.util.MultiScalePropertyFactory; + public class LauncherAnimUtils { /** * Durations for various state animations. These are not defined in resources to allow @@ -36,6 +38,7 @@ public class LauncherAnimUtils { // Progress after which the transition is assumed to be a success public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; + public static final float TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS = 0.3f; public static final IntProperty DRAWABLE_ALPHA = new IntProperty("drawableAlpha") { @@ -64,6 +67,23 @@ public class LauncherAnimUtils { } }; + /** + * Property to set the scale of workspace. The value is based on a combination + * of all the ones set, to have a smooth experience even in the case of overlapping scaling + * animation. + */ + public static final MultiScalePropertyFactory> WORKSPACE_SCALE_PROPERTY_FACTORY = + new MultiScalePropertyFactory>("workspace_scale_property"); + + /** Property to set the scale of hotseat. */ + public static final MultiScalePropertyFactory HOTSEAT_SCALE_PROPERTY_FACTORY = + new MultiScalePropertyFactory("hotseat_scale_property"); + + public static final int SCALE_INDEX_UNFOLD_ANIMATION = 1; + public static final int SCALE_INDEX_UNLOCK_ANIMATION = 2; + public static final int SCALE_INDEX_WORKSPACE_STATE = 3; + public static final int SCALE_INDEX_REVEAL_ANIM = 4; + /** Increase the duration if we prevented the fling, as we are going against a high velocity. */ public static int blockedFlingDurationFactor(float velocity) { return (int) Utilities.boundToRange(Math.abs(velocity) / 2, 2f, 6f); diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 10023b43d9..597bc8da3a 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; + import static com.android.launcher3.Utilities.getDevicePrefs; import static com.android.launcher3.config.FeatureFlags.ENABLE_THEMED_ICONS; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -36,6 +38,7 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.IconShape; import com.android.launcher3.icons.IconCache; import com.android.launcher3.icons.IconProvider; +import com.android.launcher3.icons.LauncherIconProvider; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.InstallSessionHelper; @@ -61,7 +64,7 @@ public class LauncherAppState implements SafeCloseable { private final Context mContext; private final LauncherModel mModel; - private final IconProvider mIconProvider; + private final LauncherIconProvider mIconProvider; private final IconCache mIconCache; private final InvariantDeviceProfile mInvariantDeviceProfile; private final RunnableList mOnTerminateCallback = new RunnableList(); @@ -96,9 +99,10 @@ public class LauncherAppState implements SafeCloseable { modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED, Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, - Intent.ACTION_MANAGED_PROFILE_UNLOCKED); + Intent.ACTION_MANAGED_PROFILE_UNLOCKED, + ACTION_DEVICE_POLICY_RESOURCE_UPDATED); if (FeatureFlags.IS_STUDIO_BUILD) { - modelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD); + modelChangeReceiver.register(mContext, Context.RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD); } mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver)); @@ -138,7 +142,7 @@ public class LauncherAppState implements SafeCloseable { mContext = context; mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context); - mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context)); + mIconProvider = new LauncherIconProvider(context); mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName, mIconProvider); mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext), diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java index dc533f03cf..3d2700de40 100644 --- a/src/com/android/launcher3/LauncherBackupAgent.java +++ b/src/com/android/launcher3/LauncherBackupAgent.java @@ -8,8 +8,13 @@ import android.os.ParcelFileDescriptor; import com.android.launcher3.logging.FileLog; import com.android.launcher3.provider.RestoreDbTask; +import java.io.File; +import java.io.IOException; + public class LauncherBackupAgent extends BackupAgent { + private static final String TAG = "LauncherBackupAgent"; + @Override public void onCreate() { super.onCreate(); @@ -23,6 +28,17 @@ public class LauncherBackupAgent extends BackupAgent { // Doesn't do incremental backup/restore } + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, + long mode, long mtime) throws IOException { + // Remove old files which might contain obsolete attributes like idp_grid_name in shared + // preference that will obstruct backup's attribute from writing to shared preferences. + if (destination.delete()) { + FileLog.d("LauncherBackupAgent", "Removed obsolete file: " + destination); + } + super.onRestoreFile(data, size, destination, type, mode, mtime); + } + @Override public void onBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index ee6f51ed05..de0d3002e7 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; + import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; @@ -51,6 +53,7 @@ import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.PackageIncrementalDownloadUpdatedTask; import com.android.launcher3.model.PackageInstallStateChangedTask; import com.android.launcher3.model.PackageUpdatedTask; +import com.android.launcher3.model.ReloadStringCacheTask; import com.android.launcher3.model.ShortcutsChangedTask; import com.android.launcher3.model.UserLockStateChangedTask; import com.android.launcher3.model.data.AppInfo; @@ -278,6 +281,8 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi user, Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action))); } } + } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) { + enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate)); } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) { for (Callbacks cb : getCallbacks()) { if (cb instanceof Launcher) { @@ -472,7 +477,9 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi } if (!removedIds.isEmpty()) { - deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds)); + deleteAndBindComponentsRemoved( + ItemInfoMatcher.ofItemIds(removedIds), + "removed because install session failed"); } } }); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 68e19cb85d..5aa8a46695 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -97,11 +97,13 @@ public class LauncherProvider extends ContentProvider { * Represents the schema of the database. Changes in scheme need not be backwards compatible. * When increasing the scheme version, ensure that downgrade_schema.json is updated */ - public static final int SCHEMA_VERSION = 30; + public static final int SCHEMA_VERSION = 31; public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings"; public static final String KEY_LAYOUT_PROVIDER_AUTHORITY = "KEY_LAYOUT_PROVIDER_AUTHORITY"; + private static final int TEST_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test_workspace; + static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; protected DatabaseHelper mOpenHelper; @@ -109,6 +111,8 @@ public class LauncherProvider extends ContentProvider { private long mLastRestoreTimestamp = 0L; + private boolean mUseTestWorkspaceLayout; + /** * $ adb shell dumpsys activity provider com.android.launcher3 */ @@ -158,6 +162,7 @@ public class LauncherProvider extends ContentProvider { private synchronized boolean prepForMigration(String dbFile, String targetTableName, Supplier src, Supplier dst) { if (TextUtils.equals(dbFile, mOpenHelper.getDatabaseName())) { + Log.e("b/198965093", "prepForMigration - target db is same as current: " + dbFile); return false; } @@ -390,6 +395,14 @@ public class LauncherProvider extends ContentProvider { mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); return null; } + case LauncherSettings.Settings.METHOD_SET_USE_TEST_WORKSPACE_LAYOUT_FLAG: { + mUseTestWorkspaceLayout = true; + return null; + } + case LauncherSettings.Settings.METHOD_CLEAR_USE_TEST_WORKSPACE_LAYOUT_FLAG: { + mUseTestWorkspaceLayout = false; + return null; + } case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: { loadDefaultFavoritesIfNecessary(); return null; @@ -427,7 +440,7 @@ public class LauncherProvider extends ContentProvider { Bundle result = new Bundle(); result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, prepForMigration( - InvariantDeviceProfile.INSTANCE.get(getContext()).dbFile, + arg /* dbFile */, Favorites.TMP_TABLE, () -> mOpenHelper, () -> DatabaseHelper.createDatabaseHelper( @@ -609,7 +622,8 @@ public class LauncherProvider extends ContentProvider { private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) { InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); - int defaultLayout = idp.defaultLayoutId; + int defaultLayout = mUseTestWorkspaceLayout + ? TEST_WORKSPACE_LAYOUT_RES_XML : idp.defaultLayoutId; if (getContext().getSystemService(UserManager.class).isDemoUser() && idp.demoModeLayoutId != 0) { @@ -864,6 +878,19 @@ public class LauncherProvider extends ContentProvider { Favorites.SCREEN, IntArray.wrap(-777, -778)), null); } case 30: { + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + // Clean up first row in screen 0 as it might contain junk data. + Log.d(TAG, "Cleaning up first row"); + db.delete(Favorites.TABLE_NAME, + String.format(Locale.ENGLISH, + "%1$s = %2$d AND %3$s = %4$d AND %5$s = %6$d", + Favorites.SCREEN, 0, + Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP, + Favorites.CELLY, 0), null); + } + return; + } + case 31: { // DB Upgraded successfully return; } diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java index 5ef3690ddf..a5c5c02735 100644 --- a/src/com/android/launcher3/LauncherRootView.java +++ b/src/com/android/launcher3/LauncherRootView.java @@ -1,24 +1,19 @@ package com.android.launcher3; -import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE; import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Resources; import android.graphics.Canvas; -import android.graphics.Insets; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.view.ViewDebug; import android.view.WindowInsets; -import androidx.annotation.RequiresApi; - import com.android.launcher3.graphics.SysUiScrim; import com.android.launcher3.statemanager.StatefulActivity; -import com.android.launcher3.uioverrides.ApiWrapper; +import com.android.launcher3.util.window.WindowManagerProxy; import java.util.Collections; import java.util.List; @@ -60,73 +55,12 @@ public class LauncherRootView extends InsettableFrameLayout { @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { - if (Utilities.ATLEAST_R) { - insets = updateInsetsDueToTaskbar(insets); - Insets systemWindowInsets = insets.getInsetsIgnoringVisibility( - WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); - mTempRect.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right, - systemWindowInsets.bottom); - } else { - mTempRect.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), - insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); - } + insets = WindowManagerProxy.INSTANCE.get(getContext()) + .normalizeWindowInsets(getContext(), insets, mTempRect); handleSystemWindowInsets(mTempRect); return insets; } - /** - * Taskbar provides nav bar and tappable insets. However, taskbar is not attached immediately, - * and can be destroyed and recreated. Thus, instead of relying on taskbar being present to - * get its insets, we calculate them ourselves so they are stable regardless of whether taskbar - * is currently attached. - * - * @param oldInsets The system-provided insets, which we are modifying. - * @return The updated insets. - */ - @RequiresApi(api = Build.VERSION_CODES.R) - private WindowInsets updateInsetsDueToTaskbar(WindowInsets oldInsets) { - if (!ApiWrapper.TASKBAR_DRAWN_IN_PROCESS) { - // 3P launchers based on Launcher3 should still be inset like normal. - return oldInsets; - } - - WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets); - - DeviceProfile dp = mActivity.getDeviceProfile(); - Resources resources = getResources(); - - Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); - Rect newNavInsets = new Rect(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right, - oldNavInsets.bottom); - - if (dp.isLandscape) { - boolean isGesturalMode = ResourceUtils.getIntegerByName( - "config_navBarInteractionMode", - resources, - INVALID_RESOURCE_HANDLE) == 2; - if (dp.isTablet || isGesturalMode) { - newNavInsets.bottom = ResourceUtils.getNavbarSize( - "navigation_bar_height_landscape", resources); - } else { - int navWidth = ResourceUtils.getNavbarSize("navigation_bar_width", resources); - if (dp.isSeascape()) { - newNavInsets.left = navWidth; - } else { - newNavInsets.right = navWidth; - } - } - } else { - newNavInsets.bottom = ResourceUtils.getNavbarSize("navigation_bar_height", resources); - } - updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(newNavInsets)); - updatedInsetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), - Insets.of(newNavInsets)); - - mActivity.updateWindowInsets(updatedInsetsBuilder, oldInsets); - - return updatedInsetsBuilder.build(); - } - @Override public void setInsets(Rect insets) { // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 048aaaa3bf..66195f3a1d 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -374,6 +374,12 @@ public class LauncherSettings { public static final String METHOD_CREATE_EMPTY_DB = "create_empty_db"; + public static final String METHOD_SET_USE_TEST_WORKSPACE_LAYOUT_FLAG = + "set_use_test_workspace_layout_flag"; + + public static final String METHOD_CLEAR_USE_TEST_WORKSPACE_LAYOUT_FLAG = + "clear_use_test_workspace_layout_flag"; + public static final String METHOD_LOAD_DEFAULT_FAVORITES = "load_default_favorites"; public static final String METHOD_REMOVE_GHOST_WIDGETS = "remove_ghost_widgets"; diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java index be2cd8885f..ea6a9199d7 100644 --- a/src/com/android/launcher3/LauncherState.java +++ b/src/com/android/launcher3/LauncherState.java @@ -16,6 +16,7 @@ package com.android.launcher3; import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL; @@ -79,6 +80,9 @@ public abstract class LauncherState implements BaseState { public static final int FLAG_CLOSE_POPUPS = BaseState.getFlag(6); public static final int FLAG_OVERVIEW_UI = BaseState.getFlag(7); + // Flag indicating that hotseat and its contents are not accessible. + public static final int FLAG_HOTSEAT_INACCESSIBLE = BaseState.getFlag(8); + public static final float NO_OFFSET = 0; public static final float NO_SCALE = 1; @@ -91,6 +95,14 @@ public abstract class LauncherState implements BaseState { } }; + protected static final PageTranslationProvider DEFAULT_PAGE_TRANSLATION_PROVIDER = + new PageTranslationProvider(DEACCEL_2) { + @Override + public float getPageTranslation(int pageIndex) { + return 0; + } + }; + private static final LauncherState[] sAllStates = new LauncherState[10]; /** @@ -101,7 +113,7 @@ public abstract class LauncherState implements BaseState { FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_HIDE_BACK_BUTTON | FLAG_HAS_SYS_UI_SCRIM) { @Override - public int getTransitionDuration(Context context) { + public int getTransitionDuration(Context context, boolean isToState) { // Arbitrary duration, when going to NORMAL we use the state we're coming from instead. return 0; } @@ -288,6 +300,25 @@ public abstract class LauncherState implements BaseState { }; } + /** + * Gets the translation provider for workspace pages. + */ + public PageTranslationProvider getWorkspacePageTranslationProvider(Launcher launcher) { + if (this != SPRING_LOADED || !launcher.getDeviceProfile().isTwoPanels) { + return DEFAULT_PAGE_TRANSLATION_PROVIDER; + } + final float quarterPageSpacing = launcher.getWorkspace().getPageSpacing() / 4f; + return new PageTranslationProvider(DEACCEL_2) { + @Override + public float getPageTranslation(int pageIndex) { + boolean isRtl = launcher.getWorkspace().mIsRtl; + boolean isFirstPage = pageIndex % 2 == 0; + return ((isFirstPage && !isRtl) || (!isFirstPage && isRtl)) ? -quarterPageSpacing + : quarterPageSpacing; + } + }; + } + @Override public LauncherState getHistoryForState(LauncherState previousState) { // No history is supported @@ -318,6 +349,23 @@ public abstract class LauncherState implements BaseState { public abstract float getPageAlpha(int pageIndex); } + /** + * Provider for the translation and animation interpolation of workspace pages. + */ + public abstract static class PageTranslationProvider { + + public final Interpolator interpolator; + + public PageTranslationProvider(Interpolator interpolator) { + this.interpolator = interpolator; + } + + /** + * Gets the translation of the workspace page at the provided page index. + */ + public abstract float getPageTranslation(int pageIndex); + } + public static class ScaleAndTranslation { public float scale; public float translationX; diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 2c14f07779..cba0b7d709 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -28,6 +28,8 @@ import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO import android.animation.LayoutTransition; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -78,27 +80,19 @@ public abstract class PagedView extends ViewGrou public static final int INVALID_PAGE = -1; protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE; - public static final int PAGE_SNAP_ANIMATION_DURATION = 750; - private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; // The page is moved more than halfway, automatically move to the next page on touch up. private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; private static final float MAX_SCROLL_PROGRESS = 1.0f; - // The following constants need to be scaled based on density. The scaled versions will be - // assigned to the corresponding member variables below. - private static final int FLING_THRESHOLD_VELOCITY = 500; - private static final int EASY_FLING_THRESHOLD_VELOCITY = 400; - private static final int MIN_SNAP_VELOCITY = 1500; - private static final int MIN_FLING_VELOCITY = 250; - private boolean mFreeScroll = false; - protected final int mFlingThresholdVelocity; - protected final int mEasyFlingThresholdVelocity; - protected final int mMinFlingVelocity; - protected final int mMinSnapVelocity; + private int mFlingThresholdVelocity; + private int mEasyFlingThresholdVelocity; + private int mMinFlingVelocity; + private int mMinSnapVelocity; + private int mPageSnapAnimationDuration; protected boolean mFirstLayout = true; @@ -129,7 +123,10 @@ public abstract class PagedView extends ViewGrou private boolean mAllowEasyFling; protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT; - protected int[] mPageScrolls; + private final ArrayList mOnPageScrollsInitializedCallbacks = new ArrayList<>(); + + // We should always check pageScrollsInitialized() is true when using mPageScrolls. + @Nullable protected int[] mPageScrolls = null; private boolean mIsBeingDragged; // The amount of movement to begin scrolling @@ -189,11 +186,7 @@ public abstract class PagedView extends ViewGrou mPageSlop = configuration.getScaledPagingTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - float density = getResources().getDisplayMetrics().density; - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * density); - mEasyFlingThresholdVelocity = (int) (EASY_FLING_THRESHOLD_VELOCITY * density); - mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density); - mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density); + updateVelocityValues(); initEdgeEffect(); setDefaultFocusHighlightEnabled(false); @@ -625,6 +618,22 @@ public abstract class PagedView extends ViewGrou - mInsets.left - mInsets.right; } + private void updateVelocityValues() { + Resources res = getResources(); + mFlingThresholdVelocity = res.getDimensionPixelSize(R.dimen.fling_threshold_velocity); + mEasyFlingThresholdVelocity = + res.getDimensionPixelSize(R.dimen.easy_fling_threshold_velocity); + mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity); + mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity); + mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateVelocityValues(); + } + @Override public void requestLayout() { mIsLayoutValid = false; @@ -684,26 +693,48 @@ public abstract class PagedView extends ViewGrou setMeasuredDimension(widthSize, heightSize); } + /** Returns true iff this PagedView's scroll amounts are initialized to each page index. */ + protected boolean pageScrollsInitialized() { + return mPageScrolls != null && mPageScrolls.length == getChildCount(); + } + + /** + * Queues the given callback to be run once {@code mPageScrolls} has been initialized. + */ + public void runOnPageScrollsInitialized(Runnable callback) { + mOnPageScrollsInitializedCallbacks.add(callback); + if (pageScrollsInitialized()) { + onPageScrollsInitialized(); + } + } + + private void onPageScrollsInitialized() { + for (Runnable callback : mOnPageScrollsInitializedCallbacks) { + callback.run(); + } + mOnPageScrollsInitializedCallbacks.clear(); + } + @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mIsLayoutValid = true; final int childCount = getChildCount(); + int[] pageScrolls = mPageScrolls; boolean pageScrollChanged = false; - if (mPageScrolls == null || childCount != mPageScrolls.length) { - mPageScrolls = new int[childCount]; + if (!pageScrollsInitialized()) { + pageScrolls = new int[childCount]; pageScrollChanged = true; } - if (childCount == 0) { - return; - } - if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); - boolean isScrollChanged = getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC); - if (isScrollChanged) { - pageScrollChanged = true; + pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC); + mPageScrolls = pageScrolls; + + if (childCount == 0) { + onPageScrollsInitialized(); + return; } final LayoutTransition transition = getLayoutTransition(); @@ -738,6 +769,7 @@ public abstract class PagedView extends ViewGrou if (mScroller.isFinished() && pageScrollChanged) { setCurrentPage(getNextPage()); } + onPageScrollsInitialized(); } /** @@ -775,7 +807,7 @@ public abstract class PagedView extends ViewGrou pageScrollChanged = true; outPageScrolls[i] = pageScroll; } - childStart += primaryDimension + getChildGap(); + childStart += primaryDimension + getChildGap(i, i + delta); // This makes sure that the space is added after the page, not after each panel int lastPanel = mIsRtl ? 0 : panelCount - 1; @@ -799,7 +831,7 @@ public abstract class PagedView extends ViewGrou return pageScrollChanged; } - protected int getChildGap() { + protected int getChildGap(int fromIndex, int toIndex) { return 0; } @@ -849,8 +881,10 @@ public abstract class PagedView extends ViewGrou @Override public void onViewRemoved(View child) { super.onViewRemoved(child); - mCurrentPage = validateNewPage(mCurrentPage); - mCurrentScrollOverPage = mCurrentPage; + runOnPageScrollsInitialized(() -> { + mCurrentPage = validateNewPage(mCurrentPage); + mCurrentScrollOverPage = mCurrentPage; + }); dispatchPageCountChanged(); } @@ -1153,6 +1187,8 @@ public abstract class PagedView extends ViewGrou } public int getScrollForPage(int index) { + // TODO(b/233112195): Use !pageScrollsInitialized() instead of mPageScrolls == null, once we + // root cause where we should be using runOnPageScrollsInitialized(). if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { return 0; } else { @@ -1163,7 +1199,7 @@ public abstract class PagedView extends ViewGrou // While layout transitions are occurring, a child's position may stray from its baseline // position. This method returns the magnitude of this stray at any given time. public int getLayoutTransitionOffsetForPage(int index) { - if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { + if (!pageScrollsInitialized() || index >= mPageScrolls.length || index < 0) { return 0; } else { View child = getChildAt(index); @@ -1195,8 +1231,8 @@ public abstract class PagedView extends ViewGrou mAllowOverScroll = enable; } - protected float getSignificantMoveThreshold() { - return SIGNIFICANT_MOVE_THRESHOLD; + protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) { + return absoluteDelta > pageOrientedSize * SIGNIFICANT_MOVE_THRESHOLD; } @Override @@ -1322,13 +1358,12 @@ public abstract class PagedView extends ViewGrou velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker, - mActivePointerId); + mActivePointerId); float delta = primaryDirection - mDownMotionPrimary; - delta /= mOrientationHandler.getPrimaryScale(this); - int pageOrientedSize = mOrientationHandler.getMeasuredSize(getPageAt(mCurrentPage)); - - boolean isSignificantMove = Math.abs(delta) - > pageOrientedSize * getSignificantMoveThreshold(); + int pageOrientedSize = (int) (mOrientationHandler.getMeasuredSize( + getPageAt(mCurrentPage)) + * mOrientationHandler.getPrimaryScale(this)); + boolean isSignificantMove = isSignificantMove(Math.abs(delta), pageOrientedSize); mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection); boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop; @@ -1441,7 +1476,7 @@ public abstract class PagedView extends ViewGrou return Math.abs(velocity) > threshold; } - private void resetTouchState() { + protected void resetTouchState() { releaseVelocityTracker(); mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; @@ -1590,7 +1625,7 @@ public abstract class PagedView extends ViewGrou } protected void snapToDestination() { - snapToPage(getDestinationPage(), PAGE_SNAP_ANIMATION_DURATION); + snapToPage(getDestinationPage(), mPageSnapAnimationDuration); } // We want the duration of the page snap animation to be influenced by the distance that @@ -1614,7 +1649,7 @@ public abstract class PagedView extends ViewGrou if (Math.abs(velocity) < mMinFlingVelocity) { // If the velocity is low enough, then treat this more as an automatic page advance // as opposed to an apparent physical response to flinging - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + return snapToPage(whichPage, mPageSnapAnimationDuration); } // Here we compute a "distance" that will be used in the computation of the overall @@ -1637,11 +1672,11 @@ public abstract class PagedView extends ViewGrou } public boolean snapToPage(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + return snapToPage(whichPage, mPageSnapAnimationDuration); } public boolean snapToPageImmediately(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true); + return snapToPage(whichPage, mPageSnapAnimationDuration, true); } public boolean snapToPage(int whichPage, int duration) { diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java index ece123dc62..f709acabed 100644 --- a/src/com/android/launcher3/ResourceUtils.java +++ b/src/com/android/launcher3/ResourceUtils.java @@ -28,6 +28,13 @@ public class ResourceUtils { public static final String NAVBAR_BOTTOM_GESTURE_LARGER_SIZE = "navigation_bar_gesture_larger_height"; + public static final String NAVBAR_HEIGHT = "navigation_bar_height"; + public static final String NAVBAR_HEIGHT_LANDSCAPE = "navigation_bar_height_landscape"; + + public static final String STATUS_BAR_HEIGHT = "status_bar_height"; + public static final String STATUS_BAR_HEIGHT_LANDSCAPE = "status_bar_height_landscape"; + public static final String STATUS_BAR_HEIGHT_PORTRAIT = "status_bar_height_portrait"; + public static int getNavbarSize(String resName, Resources res) { return getDimenByName(resName, res, DEFAULT_NAVBAR_VALUE); } diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index cd06414d3f..f8bc1f4e6e 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -6,8 +6,10 @@ import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURA import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION; +import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.INVALID; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED; @@ -46,6 +48,7 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PendingRequestArgs; +import com.android.launcher3.views.Snackbar; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import java.net.URISyntaxException; @@ -67,6 +70,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList private boolean mHadPendingAlarm; protected int mCurrentAccessibilityAction = -1; + public SecondaryDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -131,25 +135,34 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList return mCurrentAccessibilityAction; } + @Override + protected void setupItemInfo(ItemInfo info) { + int buttonType = getButtonType(info, getViewUnderDrag(info)); + if (buttonType != INVALID) { + setupUi(buttonType); + } + } + @Override protected boolean supportsDrop(ItemInfo info) { - return supportsAccessibilityDrop(info, getViewUnderDrag(info)); + return getButtonType(info, getViewUnderDrag(info)) != INVALID; } @Override public boolean supportsAccessibilityDrop(ItemInfo info, View view) { + return getButtonType(info, view) != INVALID; + } + + private int getButtonType(ItemInfo info, View view) { if (view instanceof AppWidgetHostView) { if (getReconfigurableWidgetId(view) != INVALID_APPWIDGET_ID) { - setupUi(RECONFIGURE); - return true; + return RECONFIGURE; } - return false; + return INVALID; } else if (FeatureFlags.ENABLE_PREDICTION_DISMISS.get() && info.isPredictedItem()) { - setupUi(DISMISS_PREDICTION); - return true; + return DISMISS_PREDICTION; } - setupUi(UNINSTALL); Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user); if (uninstallDisabled == null) { UserManager userManager = @@ -163,16 +176,20 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT); mCacheExpireAlarm.setOnAlarmListener(this); if (uninstallDisabled) { - return false; + return INVALID; } if (info instanceof ItemInfoWithIcon) { ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info; - if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0) { - return (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) != 0; + if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0 + && (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) == 0) { + return INVALID; } } - return getUninstallTarget(info) != null; + if (getUninstallTarget(info) == null) { + return INVALID; + } + return UNINSTALL; } /** @@ -220,7 +237,8 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList @Override public void completeDrop(final DragObject d) { - ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo); + ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo, + d.logInstanceId); if (d.dragSource instanceof DeferredOnComplete) { DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource; if (target != null) { @@ -264,7 +282,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList * Performs the drop action and returns the target component for the dragObject or null if * the action was not performed. */ - protected ComponentName performDropAction(View view, ItemInfo info) { + protected ComponentName performDropAction(View view, ItemInfo info, InstanceId instanceId) { if (mCurrentAccessibilityAction == RECONFIGURE) { int widgetId = getReconfigurableWidgetId(view); if (widgetId != INVALID_APPWIDGET_ID) { @@ -276,7 +294,16 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList return null; } if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { - // We sent the log event, nothing else left to do + if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) { + mLauncher.getDragLayer() + .announceForAccessibility(getContext().getString(R.string.item_removed)); + Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, () -> { }, () -> { + mStatsLogManager.logger() + .withInstanceId(instanceId) + .withItemInfo(info) + .log(LAUNCHER_DISMISS_PREDICTION_UNDO); + }); + } return null; } // else: mCurrentAccessibilityAction == UNINSTALL @@ -303,8 +330,9 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList @Override public void onAccessibilityDrop(View view, ItemInfo item) { - doLog(new InstanceIdSequence().newInstanceId(), item); - performDropAction(view, item); + InstanceId instanceId = new InstanceIdSequence().newInstanceId(); + doLog(instanceId, item); + performDropAction(view, item, instanceId); } /** diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java index 558538c48a..b81637f670 100644 --- a/src/com/android/launcher3/SessionCommitReceiver.java +++ b/src/com/android/launcher3/SessionCommitReceiver.java @@ -24,12 +24,14 @@ import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.os.UserHandle; import android.text.TextUtils; +import android.util.Log; import androidx.annotation.WorkerThread; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.pm.InstallSessionHelper; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.Executors; /** @@ -51,6 +53,9 @@ public class SessionCommitReceiver extends BroadcastReceiver { private static void processIntent(Context context, Intent intent) { if (!isEnabled(context)) { // User has decided to not add icons on homescreen. + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " not enabled"); + } return; } @@ -59,6 +64,9 @@ public class SessionCommitReceiver extends BroadcastReceiver { if (!PackageInstaller.ACTION_SESSION_COMMITTED.equals(intent.getAction()) || info == null || user == null) { // Invalid intent. + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " invalid intent"); + } return; } @@ -68,6 +76,15 @@ public class SessionCommitReceiver extends BroadcastReceiver { || info.getInstallReason() != PackageManager.INSTALL_REASON_USER || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) { packageInstallerCompat.removePromiseIconId(info.getSessionId()); + if (TestProtocol.sDebugTracing) { + int id = info.getSessionId(); + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + + ", TextUtils.isEmpty=" + TextUtils.isEmpty(info.getAppPackageName()) + + ", info.getInstallReason()=" + info.getInstallReason() + + ", INSTALL_REASON_USER=" + PackageManager.INSTALL_REASON_USER + + ", icon added=" + packageInstallerCompat.promiseIconAddedForId(id) + ); + } return; } diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index fec1d6840b..5583eaeba9 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -19,6 +19,7 @@ package com.android.launcher3; import static android.view.MotionEvent.ACTION_DOWN; import static com.android.launcher3.CellLayout.FOLDER; +import static com.android.launcher3.CellLayout.HOTSEAT; import static com.android.launcher3.CellLayout.WORKSPACE; import android.app.WallpaperManager; @@ -146,7 +147,8 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. // No need to add padding when cell layout border spacing is present. boolean noPaddingX = (dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE) - || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER); + || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER) + || (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT); int cellPaddingX = noPaddingX ? 0 : mContainerType == WORKSPACE @@ -251,7 +253,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); // While the folder is open, the position of the icon cannot change. lp.canReorder = false; - if (mContainerType == CellLayout.HOTSEAT) { + if (mContainerType == HOTSEAT) { CellLayout cl = (CellLayout) getParent(); cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY); } @@ -260,7 +262,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. @Override public void clearFolderLeaveBehind(FolderIcon child) { ((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true; - if (mContainerType == CellLayout.HOTSEAT) { + if (mContainerType == HOTSEAT) { CellLayout cl = (CellLayout) getParent(); cl.clearFolderLeaveBehind(); } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 63313f770f..7b96838dbb 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -16,7 +16,11 @@ package com.android.launcher3; +import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ICON_BADGED; +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; import android.annotation.TargetApi; import android.app.ActivityManager; @@ -36,7 +40,6 @@ import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; -import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.LightingColorFilter; @@ -48,12 +51,13 @@ import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.DeadObjectException; import android.os.Handler; import android.os.Message; +import android.os.Process; import android.os.TransactionTooLargeException; import android.provider.Settings; import android.text.Spannable; @@ -69,17 +73,15 @@ import android.view.ViewConfiguration; import android.view.animation.Interpolator; import android.widget.LinearLayout; +import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.NonNull; import androidx.core.graphics.ColorUtils; -import androidx.core.os.BuildCompat; import com.android.launcher3.dragndrop.FolderAdaptiveIcon; import com.android.launcher3.graphics.GridCustomizationsProvider; import com.android.launcher3.graphics.TintedDrawableSpan; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.FastBitmapDrawable; -import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.icons.ShortcutCachingLogic; +import com.android.launcher3.icons.ThemedIconDrawable; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.SearchActionItemInfo; @@ -88,11 +90,14 @@ import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; +import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.widget.PendingAddShortcutInfo; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -110,8 +115,6 @@ public final class Utilities { private static final Pattern sTrimPattern = Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); - private static final float[] sTmpFloatArray = new float[4]; - private static final int[] sLoc0 = new int[2]; private static final int[] sLoc1 = new int[2]; private static final Matrix sMatrix = new Matrix(); @@ -120,14 +123,20 @@ public final class Utilities { public static final String[] EMPTY_STRING_ARRAY = new String[0]; public static final Person[] EMPTY_PERSON_ARRAY = new Person[0]; + @ChecksSdkIntAtLeast(api = VERSION_CODES.P) public static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + @ChecksSdkIntAtLeast(api = VERSION_CODES.Q) public static final boolean ATLEAST_Q = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + @ChecksSdkIntAtLeast(api = VERSION_CODES.R) public static final boolean ATLEAST_R = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; - public static final boolean ATLEAST_S = BuildCompat.isAtLeastS() - || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + @ChecksSdkIntAtLeast(api = VERSION_CODES.S) + public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + + @ChecksSdkIntAtLeast(api = VERSION_CODES.TIRAMISU, codename = "T") + public static final boolean ATLEAST_T = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; /** * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}. @@ -231,7 +240,7 @@ public final class Utilities { offsetPoints(coord, v.getLeft(), v.getTop()); scale *= v.getScaleX(); - v = (View) v.getParent(); + v = v.getParent() instanceof View ? (View) v.getParent() : null; } return scale; } @@ -263,6 +272,16 @@ public final class Utilities { Math.max(points[1], points[3])); } + /** + * Similar to {@link #mapCoordInSelfToDescendant(View descendant, View root, float[] coord)} + * but accepts a Rect instead of float[]. + */ + public static void mapRectInSelfToDescendant(View descendant, View root, Rect rect) { + float[] coords = new float[]{rect.left, rect.top, rect.right, rect.bottom}; + mapCoordInSelfToDescendant(descendant, root, coords); + rect.set((int) coords[0], (int) coords[1], (int) coords[2], (int) coords[3]); + } + /** * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}. */ @@ -485,6 +504,11 @@ public final class Utilities { return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } + /** Converts a pixel value (px) to scale pixel value (SP) for the current device. */ + public static float pxToSp(float size) { + return size / Resources.getSystem().getDisplayMetrics().scaledDensity; + } + public static float dpiFromPx(float size, int densityDpi) { float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT; return (size / densityRatio); @@ -610,6 +634,10 @@ public final class Utilities { LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE); } + public static boolean isWallpaperSupported(Context context) { + return context.getSystemService(WallpaperManager.class).isWallpaperSupported(); + } + public static boolean isWallpaperAllowed(Context context) { return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed(); } @@ -673,14 +701,23 @@ public final class Utilities { /** * Returns the full drawable for info without any flattening or pre-processing. * - * @param outObj this is set to the internal data associated with {@param info}, + * @param shouldThemeIcon If true, will theme icons when applicable + * @param outObj this is set to the internal data associated with {@code info}, * eg {@link LauncherActivityInfo} or {@link ShortcutInfo}. */ + @TargetApi(Build.VERSION_CODES.TIRAMISU) public static Drawable getFullDrawable(Context context, ItemInfo info, int width, int height, - Object[] outObj) { + boolean shouldThemeIcon, Object[] outObj) { Drawable icon = loadFullDrawableWithoutTheme(context, info, width, height, outObj); - if (icon instanceof BitmapInfo.Extender) { - icon = ((BitmapInfo.Extender) icon).getThemedDrawable(context); + if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && shouldThemeIcon) { + AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon.mutate(); + Drawable mono = aid.getMonochrome(); + if (mono != null && Themes.isThemedIconEnabled(context)) { + int[] colors = ThemedIconDrawable.getColors(context); + mono = mono.mutate(); + mono.setTint(colors[1]); + return new AdaptiveIconDrawable(new ColorDrawable(colors[0]), mono); + } } return icon; } @@ -723,8 +760,7 @@ public final class Utilities { return icon; } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION && info instanceof SearchActionItemInfo) { - return new AdaptiveIconDrawable( - new FastBitmapDrawable(((SearchActionItemInfo) info).bitmap), null); + return ((SearchActionItemInfo) info).bitmap.newIcon(context); } else { return null; } @@ -739,27 +775,23 @@ public final class Utilities { @TargetApi(Build.VERSION_CODES.O) public static Drawable getBadge(Context context, ItemInfo info, Object obj) { LauncherAppState appState = LauncherAppState.getInstance(context); - int iconSize = appState.getInvariantDeviceProfile().iconBitmapSize; if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { boolean iconBadged = (info instanceof ItemInfoWithIcon) && (((ItemInfoWithIcon) info).runtimeStatusFlags & FLAG_ICON_BADGED) > 0; if ((info.id == ItemInfo.NO_ID && !iconBadged) || !(obj instanceof ShortcutInfo)) { // The item is not yet added on home screen. - return new FixedSizeEmptyDrawable(iconSize); + return new ColorDrawable(Color.TRANSPARENT); } ShortcutInfo si = (ShortcutInfo) obj; - Bitmap badge = LauncherAppState.getInstance(appState.getContext()) - .getIconCache().getShortcutInfoBadge(si).icon; - float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize); - float insetFraction = (iconSize - badgeSize) / iconSize; - return new InsetDrawable(new FastBitmapDrawable(badge), - insetFraction, insetFraction, 0, 0); + return LauncherAppState.getInstance(appState.getContext()) + .getIconCache().getShortcutInfoBadge(si).newIcon(context, FLAG_THEMED); } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { return ((FolderAdaptiveIcon) obj).getBadge(); } else { - return context.getPackageManager() - .getUserBadgedIcon(new FixedSizeEmptyDrawable(iconSize), info.user); + return Process.myUserHandle().equals(info.user) + ? new ColorDrawable(Color.TRANSPARENT) + : context.getDrawable(R.drawable.ic_work_app_badge); } } @@ -866,23 +898,38 @@ public final class Utilities { return new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight()); } - private static class FixedSizeEmptyDrawable extends ColorDrawable { - - private final int mSize; - - public FixedSizeEmptyDrawable(int size) { - super(Color.TRANSPARENT); - mSize = size; - } - - @Override - public int getIntrinsicHeight() { - return mSize; - } - - @Override - public int getIntrinsicWidth() { - return mSize; + /** + * Returns a list of screen-splitting options depending on the device orientation (split top for + * portrait, split left for landscape, split left and right for landscape tablets, etc.) + */ + public static List getSplitPositionOptions( + DeviceProfile dp) { + List options = new ArrayList<>(); + // Add both left and right options if we're in tablet mode + if (dp.isTablet && dp.isLandscape) { + options.add(new SplitPositionOption( + R.drawable.ic_split_left, R.string.split_screen_position_left, + STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); + options.add(new SplitPositionOption( + R.drawable.ic_split_right, R.string.split_screen_position_right, + STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); + } else { + if (dp.isSeascape()) { + // Add left/right options + options.add(new SplitPositionOption( + R.drawable.ic_split_right, R.string.split_screen_position_right, + STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); + } else if (dp.isLandscape) { + options.add(new SplitPositionOption( + R.drawable.ic_split_left, R.string.split_screen_position_left, + STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); + } else { + // Only add top option + options.add(new SplitPositionOption( + R.drawable.ic_split_top, R.string.split_screen_position_top, + STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); + } } + return options; } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index f18ff3bee3..4903d7758a 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -50,7 +50,6 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.os.Parcelable; -import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -86,14 +85,12 @@ import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.LauncherEvent; -import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; -import com.android.launcher3.model.data.SearchActionItemInfo; +import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.pageindicators.WorkspacePageIndicator; -import com.android.launcher3.popup.PopupContainerWithArrow; +import com.android.launcher3.pageindicators.PageIndicator; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; @@ -103,7 +100,6 @@ import com.android.launcher3.util.Executors; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.IntSparseArrayMap; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.OverlayEdgeEffect; import com.android.launcher3.util.PackageUserKey; @@ -113,6 +109,7 @@ import com.android.launcher3.util.WallpaperOffsetInterpolator; import com.android.launcher3.widget.LauncherAppWidgetHost; import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener; import com.android.launcher3.widget.LauncherAppWidgetHostView; +import com.android.launcher3.widget.NavigableAppWidgetHostView; import com.android.launcher3.widget.PendingAddShortcutInfo; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.PendingAppWidgetHostView; @@ -122,7 +119,6 @@ import com.android.launcher3.widget.util.WidgetSizes; import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; @@ -133,8 +129,9 @@ import java.util.stream.Collectors; * The workspace is a wide area with a wallpaper and a finite number of pages. * Each page contains a number of icons, folders or widgets the user can * interact with. A workspace is meant to be used with a fixed width only. + * @param Class that extends View and PageIndicator */ -public class Workspace extends PagedView +public class Workspace extends PagedView implements DropTarget, DragSource, View.OnTouchListener, DragController.DragListener, Insettable, StateHandler, WorkspaceLayoutManager, LauncherBindableItemsContainer { @@ -147,6 +144,8 @@ public class Workspace extends PagedView * {@link #isFinishedSwitchingState()} ()} to return true. */ private static final float FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS = 0.5f; + private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f; + private static final boolean ENFORCE_DRAG_EVENT_ORDER = false; private static final int ADJACENT_SCREEN_DROP_DURATION = 300; @@ -198,6 +197,7 @@ public class Workspace extends PagedView private final int[] mTempXY = new int[2]; private final float[] mTempFXY = new float[2]; + private final Rect mTempRect = new Rect(); @Thunk float[] mDragViewVisualCenter = new float[2]; private SpringLoadedDragController mSpringLoadedDragController; @@ -324,37 +324,14 @@ public class Workspace extends PagedView setPageSpacing(Math.max(maxInsets, maxPadding)); } - updateWorkspaceScreensPadding(); + updateCellLayoutPadding(); updateWorkspaceWidgetsSizes(); } - private void updateWorkspaceScreensPadding() { - DeviceProfile grid = mLauncher.getDeviceProfile(); - int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx; - int paddingBottom = grid.cellLayoutBottomPaddingPx; - - int panelCount = getPanelCount(); - int rightPanelModulus = mIsRtl ? 0 : panelCount - 1; - int leftPanelModulus = mIsRtl ? panelCount - 1 : 0; - int numberOfScreens = mScreenOrder.size(); - for (int i = 0; i < numberOfScreens; i++) { - int paddingLeft = paddingLeftRight; - int paddingRight = paddingLeftRight; - // Add missing cellLayout border in-between panels. - if (panelCount > 1) { - if (i % panelCount == leftPanelModulus) { - paddingRight += grid.cellLayoutBorderSpacePx.x / 2; - } else if (i % panelCount == rightPanelModulus) { // right side panel - paddingLeft += grid.cellLayoutBorderSpacePx.x / 2; - } else { // middle panel - paddingLeft += grid.cellLayoutBorderSpacePx.x / 2; - paddingRight += grid.cellLayoutBorderSpacePx.x / 2; - } - } - // SparseArrayMap doesn't keep the order - mWorkspaceScreens.get(mScreenOrder.get(i)) - .setPadding(paddingLeft, 0, paddingRight, paddingBottom); - } + private void updateCellLayoutPadding() { + Rect padding = mLauncher.getDeviceProfile().cellLayoutPaddingPx; + mWorkspaceScreens.forEach( + s -> s.setPadding(padding.left, padding.top, padding.right, padding.bottom)); } private void updateWorkspaceWidgetsSizes() { @@ -586,8 +563,8 @@ public class Workspace extends PagedView int cellVSpan = FeatureFlags.EXPANDED_SMARTSPACE.get() ? EXPANDED_SMARTSPACE_HEIGHT : DEFAULT_SMARTSPACE_HEIGHT; - CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), - cellVSpan); + int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns; + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, cellHSpan, cellVSpan); lp.canReorder = false; if (!firstPage.addViewToCellLayout(mQsb, 0, R.id.search_container_workspace, lp, true)) { Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout"); @@ -652,7 +629,7 @@ public class Workspace extends PagedView mLauncher.getStateManager().getState(), newScreen, insertIndex); updatePageScrollValues(); - updateWorkspaceScreensPadding(); + updateCellLayoutPadding(); return newScreen; } @@ -927,7 +904,11 @@ public class Workspace extends PagedView * two panel UI is enabled. */ public int getScreenPair(int screenId) { - if (screenId % 2 == 0) { + if (screenId == EXTRA_EMPTY_SCREEN_ID) { + return EXTRA_EMPTY_SCREEN_SECOND_ID; + } else if (screenId == EXTRA_EMPTY_SCREEN_SECOND_ID) { + return EXTRA_EMPTY_SCREEN_ID; + } else if (screenId % 2 == 0) { return screenId + 1; } else { return screenId - 1; @@ -1136,6 +1117,10 @@ public class Workspace extends PagedView stripEmptyScreens(); mStripScreensOnPageStopMoving = false; } + + // Inform the Launcher activity that the page transition ended so that it can react to the + // newly visible page if it wants to. + mLauncher.onPageEndTransition(); } public void setLauncherOverlay(LauncherOverlay overlay) { @@ -1212,6 +1197,10 @@ public class Workspace extends PagedView .log(LAUNCHER_SWIPELEFT); } mOverlayShown = true; + + // Let the Launcher activity know that the overlay is now visible. + mLauncher.onOverlayVisibilityChanged(mOverlayShown); + // Not announcing the overlay page for accessibility since it announces itself. } else if (Float.compare(scroll, 0f) == 0) { if (mOverlayShown) { @@ -1235,6 +1224,10 @@ public class Workspace extends PagedView announcePageForAccessibility(); } mOverlayShown = false; + + // Let the Launcher activity know that the overlay is no longer visible. + mLauncher.onOverlayVisibilityChanged(mOverlayShown); + tryRunOverlayCallback(); } @@ -1685,11 +1678,7 @@ public class Workspace extends PagedView } if (child instanceof BubbleTextView && !dragOptions.isAccessibleDrag) { - PopupContainerWithArrow popupContainer = PopupContainerWithArrow - .showForIcon((BubbleTextView) child); - if (popupContainer != null) { - dragOptions.preDragCondition = popupContainer.createPreDragCondition(true); - } + dragOptions.preDragCondition = ((BubbleTextView) child).startLongPressAction(); } final DragView dv; @@ -1739,7 +1728,7 @@ public class Workspace extends PagedView // If it's an external drop (e.g. from All Apps), check if it should be accepted CellLayout dropTargetLayout = mDropToLayout; if (d.dragSource != this) { - // Don't accept the drop if we're not over a screen at time of drop + // Don't accept the drop if we're not over a valid drop target at time of drop if (dropTargetLayout == null) { return false; } @@ -2122,7 +2111,7 @@ public class Workspace extends PagedView final Runnable onCompleteCallback = onCompleteRunnable; mLauncher.getDragController().animateDragViewToOriginalPosition( /* onComplete= */ callbackList::executeAllAndDestroy, cell, - SPRING_LOADED.getTransitionDuration(mLauncher)); + SPRING_LOADED.getTransitionDuration(mLauncher, true /* isToState */)); mLauncher.getStateManager().goToState(NORMAL, /* delay= */ 0, onCompleteCallback == null ? null @@ -2340,17 +2329,6 @@ public class Workspace extends PagedView xy[1] = xy[1] - v.getTop(); } - boolean isPointInSelfOverHotseat(int x, int y) { - mTempFXY[0] = x; - mTempFXY[1] = y; - mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempFXY, true); - View hotseat = mLauncher.getHotseat(); - return mTempFXY[0] >= hotseat.getLeft() - && mTempFXY[0] <= hotseat.getRight() - && mTempFXY[1] >= hotseat.getTop() - && mTempFXY[1] <= hotseat.getBottom(); - } - /** * Updates the point in {@param xy} to point to the co-ordinate space of {@param layout} * @param layout either hotseat of a page in workspace @@ -2388,7 +2366,7 @@ public class Workspace extends PagedView final View child = (mDragInfo == null) ? null : mDragInfo.cell; if (setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1])) { - if (mLauncher.isHotseatLayout(mDragTargetLayout)) { + if (mDragTargetLayout == null || mLauncher.isHotseatLayout(mDragTargetLayout)) { mSpringLoadedDragController.cancel(); } else { mSpringLoadedDragController.setAlarm(mDragTargetLayout); @@ -2467,42 +2445,25 @@ public class Workspace extends PagedView */ private boolean setDropLayoutForDragObject(DragObject d, float centerX, float centerY) { CellLayout layout = null; - // Test to see if we are over the hotseat first - if (mLauncher.getHotseat() != null && !isDragWidget(d)) { - if (isPointInSelfOverHotseat(d.x, d.y)) { - layout = mLauncher.getHotseat(); + if (shouldUseHotseatAsDropLayout(d)) { + layout = mLauncher.getHotseat(); + } else if (!isDragObjectOverSmartSpace(d)) { + // If the object is over qsb/smartspace, we don't want to highlight anything. + + // Check neighbour pages + layout = checkDragObjectIsOverNeighbourPages(d, centerX); + + if (layout == null) { + // Check visible pages + IntSet visiblePageIndices = getVisiblePageIndices(); + for (int visiblePageIndex : visiblePageIndices) { + layout = verifyInsidePage(visiblePageIndex, d.x, d.y); + if (layout != null) break; + } } } - int nextPage = getNextPage(); - IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1, nextPage + 1); - if (isTwoPanelEnabled()) { - // If two panel is enabled, users can also drag items to nextPage + 2 - pageIndexesToVerify.add(nextPage + 2); - } - - int touchX = (int) Math.min(centerX, d.x); - int touchY = d.y; - - // Go through the pages and check if the dragged item is inside one of them - for (int pageIndex : pageIndexesToVerify) { - if (layout != null || isPageInTransition()) { - break; - } - layout = verifyInsidePage(pageIndex, touchX, touchY); - } - - // If the dragged item isn't located in one of the pages above, the icon will stay on the - // current screen. For two panel pick the closest panel on the current screen, - // on one panel just choose the current page. - if (layout == null && nextPage >= 0 && nextPage < getPageCount()) { - if (isTwoPanelEnabled()) { - nextPage = getScreenCenter(getScrollX()) > touchX - ? (mIsRtl ? nextPage + 1 : nextPage) // left side - : (mIsRtl ? nextPage : nextPage + 1); // right side - } - layout = (CellLayout) getChildAt(nextPage); - } + // Update the current drop layout if the target changed if (layout != mDragTargetLayout) { setCurrentDropLayout(layout); setCurrentDragOverlappingLayout(layout); @@ -2511,6 +2472,69 @@ public class Workspace extends PagedView return false; } + private boolean shouldUseHotseatAsDropLayout(DragObject dragObject) { + if (mLauncher.getHotseat() == null + || mLauncher.getHotseat().getShortcutsAndWidgets() == null + || isDragWidget(dragObject)) { + return false; + } + View hotseatShortcuts = mLauncher.getHotseat().getShortcutsAndWidgets(); + getViewBoundsRelativeToWorkspace(hotseatShortcuts, mTempRect); + return mTempRect.contains(dragObject.x, dragObject.y); + } + + private boolean isDragObjectOverSmartSpace(DragObject dragObject) { + if (mQsb == null) { + return false; + } + getViewBoundsRelativeToWorkspace(mQsb, mTempRect); + return mTempRect.contains(dragObject.x, dragObject.y); + } + + private CellLayout checkDragObjectIsOverNeighbourPages(DragObject d, float centerX) { + if (isPageInTransition()) { + return null; + } + + // Check the workspace pages whether the object is over any of them + + // Note, centerX represents the center of the object that is being dragged, visually. + // d.x represents the location of the finger within the dragged item. + float touchX; + float touchY = d.y; + + // Go through the pages and check if the dragged item is inside one of them. This block + // is responsible for determining whether we need to snap to a different screen. + int nextPage = getNextPage(); + IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1, + nextPage + (isTwoPanelEnabled() ? 2 : 1)); + + for (int pageIndex : pageIndexesToVerify) { + // When deciding whether to perform a page switch, we need to consider the most + // extreme X coordinate between the finger location and the center of the object + // being dragged. This is either the max or the min of the two depending on whether + // dragging to the left / right, respectively. + touchX = (((pageIndex < nextPage) && !mIsRtl) || (pageIndex > nextPage && mIsRtl)) + ? Math.min(d.x, centerX) : Math.max(d.x, centerX); + CellLayout layout = verifyInsidePage(pageIndex, touchX, touchY); + if (layout != null) { + return layout; + } + } + return null; + } + + /** + * Gets the given view's bounds relative to Workspace + */ + private void getViewBoundsRelativeToWorkspace(View view, Rect outRect) { + mLauncher.getDragLayer() + .getDescendantRectRelativeToSelf(view, mTempRect); + // map draglayer relative bounds to workspace + mLauncher.getDragLayer().mapRectInSelfToDescendant(this, mTempRect); + outRect.set(mTempRect); + } + /** * Returns the child CellLayout if the point is inside the page coordinates, null otherwise. */ @@ -2747,14 +2771,15 @@ public class Workspace extends PagedView case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION: - if (info instanceof AppInfo) { + if (info instanceof WorkspaceItemFactory) { // Came from all apps -- make a copy - info = ((AppInfo) info).makeWorkspaceItem(); + info = ((WorkspaceItemFactory) info).makeWorkspaceItem(mLauncher); d.dragInfo = info; } - if (info instanceof SearchActionItemInfo) { - info = ((SearchActionItemInfo) info).createWorkspaceItem( - mLauncher.getModel()); + if (info instanceof WorkspaceItemInfo + && info.container == LauncherSettings.Favorites.CONTAINER_PREDICTION) { + // Came from all apps prediction row -- make a copy + info = new WorkspaceItemInfo((WorkspaceItemInfo) info); d.dragInfo = info; } view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info); @@ -2832,7 +2857,8 @@ public class Workspace extends PagedView } private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY, - DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale) { + DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale, + final View finalView) { // Now we animate the dragView, (ie. the widget or shortcut preview) into its final // location and size on the home screen. int spanX = info.spanX; @@ -2841,6 +2867,14 @@ public class Workspace extends PagedView Rect r = estimateItemPosition(layout, targetCell[0], targetCell[1], spanX, spanY); if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { DeviceProfile profile = mLauncher.getDeviceProfile(); + if (profile.shouldInsetWidgets() && finalView instanceof NavigableAppWidgetHostView) { + Rect widgetPadding = new Rect(); + ((NavigableAppWidgetHostView) finalView).getWidgetInset(profile, widgetPadding); + r.left -= widgetPadding.left; + r.right += widgetPadding.right; + r.top -= widgetPadding.top; + r.bottom += widgetPadding.bottom; + } Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y); } @@ -2887,7 +2921,7 @@ public class Workspace extends PagedView float scaleXY[] = new float[2]; boolean scalePreview = !(info instanceof PendingAddShortcutInfo); getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell, - scalePreview); + scalePreview, finalView); Resources res = mLauncher.getResources(); final int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200; @@ -3034,7 +3068,8 @@ public class Workspace extends PagedView if (info instanceof LauncherAppWidgetInfo) { LauncherAppWidgetInfo appWidgetInfo = (LauncherAppWidgetInfo) info; if (appWidgetInfo.appWidgetId == appWidgetId) { - mLauncher.removeItem(view, appWidgetInfo, true); + mLauncher.removeItem(view, appWidgetInfo, true, + "widget is removed in response to widget remove broadcast"); return true; } } @@ -3189,7 +3224,7 @@ public class Workspace extends PagedView * as a part of an update, this is called to ensure that other widgets and application * shortcuts are not removed. */ - public void removeItemsByMatcher(final ItemInfoMatcher matcher) { + public void removeItemsByMatcher(final Predicate matcher) { for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) { ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets(); // Iterate in reverse order as we are removing items @@ -3197,7 +3232,7 @@ public class Workspace extends PagedView View child = container.getChildAt(i); ItemInfo info = (ItemInfo) child.getTag(); - if (matcher.matchesInfo(info)) { + if (matcher.test(info)) { layout.removeViewInLayout(child); if (child instanceof DropTarget) { mDragController.removeDropTarget((DropTarget) child); @@ -3205,7 +3240,7 @@ public class Workspace extends PagedView } else if (child instanceof FolderIcon) { FolderInfo folderInfo = (FolderInfo) info; List matches = folderInfo.contents.stream() - .filter(matcher::matchesInfo) + .filter(matcher) .collect(Collectors.toList()); if (!matches.isEmpty()) { folderInfo.removeAll(matches, false); @@ -3230,7 +3265,11 @@ public class Workspace extends PagedView } } - private View mapOverCellLayout(CellLayout layout, ItemOperator op) { + /** + * Perform {param operator} over all the items in a given {param layout}. + * @return The first item that satisfies the operator or null. + */ + public View mapOverCellLayout(CellLayout layout, ItemOperator operator) { // TODO(b/128460496) Potential race condition where layout is not yet loaded if (layout == null) { return null; @@ -3240,7 +3279,7 @@ public class Workspace extends PagedView final int itemCount = container.getChildCount(); for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { View item = container.getChildAt(itemIdx); - if (op.evaluate((ItemInfo) item.getTag(), item)) { + if (operator.evaluate((ItemInfo) item.getTag(), item)) { return item; } } @@ -3279,10 +3318,14 @@ public class Workspace extends PagedView } } - public void removeAbandonedPromise(String packageName, UserHandle user) { - ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages( - Collections.singleton(packageName), user); - mLauncher.getModelWriter().deleteItemsFromDatabase(matcher); + /** + * Remove workspace icons & widget information related to items in matcher. + * + * @param matcher the matcher generated by the caller. + */ + public void persistRemoveItemsByMatcher(Predicate matcher, + @Nullable final String reason) { + mLauncher.getModelWriter().deleteItemsFromDatabase(matcher, reason); removeItemsByMatcher(matcher); } @@ -3388,7 +3431,21 @@ public class Workspace extends PagedView // When the workspace is not loaded, we do not know how many screen will be bound. return getContext().getString(R.string.home_screen); } - return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens); + int panelCount = getPanelCount(); + int currentPage = (page / panelCount) + 1; + int totalPages = nScreens / panelCount + nScreens % panelCount; + return getContext().getString(R.string.workspace_scroll_format, currentPage, totalPages); + } + + @Override + protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) { + DeviceProfile deviceProfile = mLauncher.getDeviceProfile(); + if (!deviceProfile.isTablet) { + return super.isSignificantMove(absoluteDelta, pageOrientedSize); + } + + return absoluteDelta + > deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE; } /** diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java index 1b9647afc0..a991c2f959 100644 --- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -18,11 +18,14 @@ package com.android.launcher3; import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE; -import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY; +import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; +import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY; import static com.android.launcher3.LauncherState.FLAG_HAS_SYS_UI_SCRIM; +import static com.android.launcher3.LauncherState.FLAG_HOTSEAT_INACCESSIBLE; import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; import static com.android.launcher3.LauncherState.NORMAL; @@ -33,19 +36,23 @@ import static com.android.launcher3.anim.Interpolators.ZOOM_OUT; import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS; import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_PAGE_TRANSLATE_X; import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE; import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM; import android.animation.ValueAnimator; +import android.util.FloatProperty; import android.view.View; import android.view.animation.Interpolator; import com.android.launcher3.LauncherState.PageAlphaProvider; +import com.android.launcher3.LauncherState.PageTranslationProvider; import com.android.launcher3.LauncherState.ScaleAndTranslation; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; @@ -62,12 +69,18 @@ import com.android.systemui.plugins.ResourceProvider; */ public class WorkspaceStateTransitionAnimation { + private static final FloatProperty> WORKSPACE_SCALE_PROPERTY = + WORKSPACE_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WORKSPACE_STATE); + + private static final FloatProperty HOTSEAT_SCALE_PROPERTY = + HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WORKSPACE_STATE); + private final Launcher mLauncher; - private final Workspace mWorkspace; + private final Workspace mWorkspace; private float mNewScale; - public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { + public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { mLauncher = launcher; mWorkspace = workspace; } @@ -105,8 +118,6 @@ public class WorkspaceStateTransitionAnimation { } int elements = state.getVisibleElements(mLauncher); - Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE, - pageAlphaProvider.interpolator); Hotseat hotseat = mWorkspace.getHotseat(); Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT); LauncherState fromState = mLauncher.getStateManager().getState(); @@ -115,28 +126,40 @@ public class WorkspaceStateTransitionAnimation { && fromState == HINT_STATE && state == NORMAL; if (shouldSpring) { ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher, - mWorkspace, mNewScale)); + mWorkspace, mNewScale, WORKSPACE_SCALE_PROPERTY)); } else { - propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator); + propertySetter.setFloat(mWorkspace, WORKSPACE_SCALE_PROPERTY, mNewScale, + scaleInterpolator); } mWorkspace.setPivotToScaleWithSelf(hotseat); float hotseatScale = hotseatScaleAndTranslation.scale; if (shouldSpring) { PendingAnimation pa = (PendingAnimation) propertySetter; - pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale)); + pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale, + HOTSEAT_SCALE_PROPERTY)); } else { Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE, scaleInterpolator); - propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale, + propertySetter.setFloat(hotseat, HOTSEAT_SCALE_PROPERTY, hotseatScale, hotseatScaleInterpolator); } - float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0; - propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator); + Interpolator workspaceFadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE, + pageAlphaProvider.interpolator); float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0; propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(), - workspacePageIndicatorAlpha, fadeInterpolator); + workspacePageIndicatorAlpha, workspaceFadeInterpolator); + Interpolator hotseatFadeInterpolator = config.getInterpolator(ANIM_HOTSEAT_FADE, + workspaceFadeInterpolator); + float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0; + propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, hotseatFadeInterpolator); + + // Update the accessibility flags for hotseat based on launcher state. + hotseat.setImportantForAccessibility( + state.hasFlag(FLAG_HOTSEAT_INACCESSIBLE) + ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); Interpolator translationInterpolator = config.getInterpolator(ANIM_WORKSPACE_TRANSLATE, ZOOM_OUT); @@ -144,6 +167,12 @@ public class WorkspaceStateTransitionAnimation { scaleAndTranslation.translationX, translationInterpolator); propertySetter.setFloat(mWorkspace, VIEW_TRANSLATE_Y, scaleAndTranslation.translationY, translationInterpolator); + PageTranslationProvider pageTranslationProvider = state.getWorkspacePageTranslationProvider( + mLauncher); + for (int i = 0; i < childCount; i++) { + applyPageTranslation((CellLayout) mWorkspace.getChildAt(i), i, pageTranslationProvider, + propertySetter, config); + } Interpolator hotseatTranslationInterpolator = config.getInterpolator( ANIM_HOTSEAT_TRANSLATE, translationInterpolator); @@ -191,10 +220,29 @@ public class WorkspaceStateTransitionAnimation { pageAlpha, fadeInterpolator); } + private void applyPageTranslation(CellLayout cellLayout, int childIndex, + PageTranslationProvider pageTranslationProvider, PropertySetter propertySetter, + StateAnimationConfig config) { + float pageTranslation = pageTranslationProvider.getPageTranslation(childIndex); + Interpolator translationInterpolator = config.getInterpolator( + ANIM_WORKSPACE_PAGE_TRANSLATE_X, pageTranslationProvider.interpolator); + propertySetter.setFloat(cellLayout, VIEW_TRANSLATE_X, pageTranslation, + translationInterpolator); + } + + /** + * Returns a spring based animator for the scale property of {@param workspace}. + */ + public static ValueAnimator getWorkspaceSpringScaleAnimator(Launcher launcher, + Workspace workspace, float scale) { + return getSpringScaleAnimator(launcher, workspace, scale, WORKSPACE_SCALE_PROPERTY); + } + /** * Returns a spring based animator for the scale property of {@param v}. */ - public static ValueAnimator getSpringScaleAnimator(Launcher launcher, View v, float scale) { + public static ValueAnimator getSpringScaleAnimator(Launcher launcher, T v, + float scale, FloatProperty property) { ResourceProvider rp = DynamicResource.provider(launcher); float damping = rp.getFloat(R.dimen.hint_scale_damping_ratio); float stiffness = rp.getFloat(R.dimen.hint_scale_stiffness); @@ -205,9 +253,9 @@ public class WorkspaceStateTransitionAnimation { .setDampingRatio(damping) .setMinimumVisibleChange(MIN_VISIBLE_CHANGE_SCALE) .setEndValue(scale) - .setStartValue(SCALE_PROPERTY.get(v)) + .setStartValue(property.get(v)) .setStartVelocity(velocityPxPerS) - .build(v, SCALE_PROPERTY); + .build(v, property); } } \ No newline at end of file diff --git a/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java new file mode 100644 index 0000000000..19d042147b --- /dev/null +++ b/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.accessibility; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DropTarget; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.dragndrop.DragController; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BubbleTextHolder; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseAccessibilityDelegate + extends View.AccessibilityDelegate implements DragController.DragListener { + + public enum DragType { + ICON, + FOLDER, + WIDGET + } + + public static class DragInfo { + public DragType dragType; + public ItemInfo info; + public View item; + } + + protected final SparseArray mActions = new SparseArray<>(); + protected final T mContext; + + protected DragInfo mDragInfo = null; + + protected BaseAccessibilityDelegate(T context) { + mContext = context; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (host.getTag() instanceof ItemInfo) { + ItemInfo item = (ItemInfo) host.getTag(); + + List actions = new ArrayList<>(); + getSupportedActions(host, item, actions); + actions.forEach(la -> info.addAction(la.accessibilityAction)); + + if (!itemSupportsLongClick(host)) { + info.setLongClickable(false); + info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); + } + } + } + + /** + * Adds all the accessibility actions that can be handled. + */ + protected abstract void getSupportedActions(View host, ItemInfo item, List out); + + private boolean itemSupportsLongClick(View host) { + if (host instanceof BubbleTextView) { + return ((BubbleTextView) host).canShowLongPressPopup(); + } else if (host instanceof BubbleTextHolder) { + BubbleTextHolder holder = (BubbleTextHolder) host; + return holder.getBubbleText() != null && holder.getBubbleText().canShowLongPressPopup(); + } else { + return false; + } + } + + protected boolean itemSupportsAccessibleDrag(ItemInfo item) { + if (item instanceof WorkspaceItemInfo) { + // Support the action unless the item is in a context menu. + return item.screenId >= 0 + && item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; + } + return (item instanceof LauncherAppWidgetInfo) + || (item instanceof FolderInfo); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if ((host.getTag() instanceof ItemInfo) + && performAction(host, (ItemInfo) host.getTag(), action, false)) { + return true; + } + return super.performAccessibilityAction(host, action, args); + } + + protected abstract boolean performAction( + View host, ItemInfo item, int action, boolean fromKeyboard); + + @Thunk + protected void announceConfirmation(String confirmation) { + mContext.getDragLayer().announceForAccessibility(confirmation); + } + + public boolean isInAccessibleDrag() { + return mDragInfo != null; + } + + public DragInfo getDragInfo() { + return mDragInfo; + } + + /** + * @param clickedTarget the actual view that was clicked + * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used + * as the actual drop location otherwise the views center is used. + */ + public void handleAccessibleDrop(View clickedTarget, Rect dropLocation, + String confirmation) { + if (!isInAccessibleDrag()) return; + + int[] loc = new int[2]; + if (dropLocation == null) { + loc[0] = clickedTarget.getWidth() / 2; + loc[1] = clickedTarget.getHeight() / 2; + } else { + loc[0] = dropLocation.centerX(); + loc[1] = dropLocation.centerY(); + } + + mContext.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc); + mContext.getDragController().completeAccessibleDrag(loc); + + if (!TextUtils.isEmpty(confirmation)) { + announceConfirmation(confirmation); + } + } + + protected abstract boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard); + + + @Override + public void onDragEnd() { + mContext.getDragController().removeDragListener(this); + mDragInfo = null; + } + + @Override + public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { + // No-op + } + + public class LauncherAction { + public final int keyCode; + public final AccessibilityNodeInfo.AccessibilityAction accessibilityAction; + + private final BaseAccessibilityDelegate mDelegate; + + public LauncherAction(int id, int labelRes, int keyCode) { + this.keyCode = keyCode; + accessibilityAction = new AccessibilityNodeInfo.AccessibilityAction( + id, mContext.getString(labelRes)); + mDelegate = BaseAccessibilityDelegate.this; + } + + /** + * Invokes the action for the provided host + */ + public boolean invokeFromKeyboard(View host) { + if (host != null && host.getTag() instanceof ItemInfo) { + return mDelegate.performAction( + host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true); + } else { + return false; + } + } + } +} diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 157df5d070..79214e896a 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -10,35 +10,28 @@ import android.appwidget.AppWidgetProviderInfo; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -import android.os.Bundle; import android.os.Handler; -import android.text.TextUtils; import android.util.Log; -import android.util.SparseArray; import android.view.KeyEvent; import android.view.View; -import android.view.View.AccessibilityDelegate; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.launcher3.BubbleTextView; import com.android.launcher3.ButtonDropTarget; import com.android.launcher3.CellLayout; -import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.LauncherSettings; import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.R; import com.android.launcher3.Workspace; -import com.android.launcher3.dragndrop.DragController.DragListener; import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.folder.Folder; import com.android.launcher3.keyboard.KeyboardDragAndDropView; -import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.popup.ArrowPopup; @@ -48,6 +41,7 @@ import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.Thunk; +import com.android.launcher3.views.BubbleTextHolder; import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.views.OptionsPopupView.OptionItem; import com.android.launcher3.widget.LauncherAppWidgetHostView; @@ -57,7 +51,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener { +public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate { private static final String TAG = "LauncherAccessibilityDelegate"; @@ -66,6 +60,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme public static final int DISMISS_PREDICTION = R.id.action_dismiss_prediction; public static final int PIN_PREDICTION = R.id.action_pin_prediction; public static final int RECONFIGURE = R.id.action_reconfigure; + public static final int INVALID = -1; protected static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; protected static final int MOVE = R.id.action_move; protected static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace; @@ -73,25 +68,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme public static final int DEEP_SHORTCUTS = R.id.action_deep_shortcuts; public static final int SHORTCUTS_AND_NOTIFICATIONS = R.id.action_shortcuts_and_notifications; - public enum DragType { - ICON, - FOLDER, - WIDGET - } - - public static class DragInfo { - public DragType dragType; - public ItemInfo info; - public View item; - } - - protected final SparseArray mActions = new SparseArray<>(); - protected final Launcher mLauncher; - - private DragInfo mDragInfo = null; - public LauncherAccessibilityDelegate(Launcher launcher) { - mLauncher = launcher; + super(launcher); mActions.put(REMOVE, new LauncherAction( REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X)); @@ -116,25 +94,6 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - if (host.getTag() instanceof ItemInfo) { - ItemInfo item = (ItemInfo) host.getTag(); - - List actions = new ArrayList<>(); - getSupportedActions(host, item, actions); - actions.forEach(la -> info.addAction(la.accessibilityAction)); - - if (!itemSupportsLongClick(host, item)) { - info.setLongClickable(false); - info.removeAction(AccessibilityAction.ACTION_LONG_CLICK); - } - } - } - - /** - * Adds all the accessibility actions that can be handled. - */ protected void getSupportedActions(View host, ItemInfo item, List out) { // If the request came from keyboard, do not add custom shortcuts as that is already // exposed as a direct shortcut @@ -143,7 +102,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS)); } - for (ButtonDropTarget target : mLauncher.getDropTargetBar().getDropTargets()) { + for (ButtonDropTarget target : mContext.getDropTargetBar().getDropTargets()) { if (target.supportsAccessibilityDrop(item, host)) { out.add(mActions.get(target.getAccessibilityAction())); } @@ -162,7 +121,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } } - if ((item instanceof AppInfo) || (item instanceof WorkspaceItemInfo) + if ((item instanceof WorkspaceItemFactory) || (item instanceof WorkspaceItemInfo) || (item instanceof PendingAddItemInfo)) { out.add(mActions.get(ADD_TO_WORKSPACE)); } @@ -183,112 +142,44 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return result; } - private boolean itemSupportsLongClick(View host, ItemInfo info) { - return PopupContainerWithArrow.canShow(host, info); - } - - private boolean itemSupportsAccessibleDrag(ItemInfo item) { - if (item instanceof WorkspaceItemInfo) { - // Support the action unless the item is in a context menu. - return item.screenId >= 0 && item.container != Favorites.CONTAINER_HOTSEAT_PREDICTION; - } - return (item instanceof LauncherAppWidgetInfo) - || (item instanceof FolderInfo); - } - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if ((host.getTag() instanceof ItemInfo) - && performAction(host, (ItemInfo) host.getTag(), action, false)) { - return true; - } - return super.performAccessibilityAction(host, action, args); - } - - /** - * Performs the provided action on the host - */ protected boolean performAction(final View host, final ItemInfo item, int action, boolean fromKeyboard) { if (action == ACTION_LONG_CLICK) { - if (PopupContainerWithArrow.canShow(host, item)) { - // Long press should be consumed for workspace items, and it should invoke the - // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the - // standard long press path does. - PopupContainerWithArrow.showForIcon((BubbleTextView) host); - return true; + PreDragCondition dragCondition = null; + // Long press should be consumed for workspace items, and it should invoke the + // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the + // standard long press path does. + if (host instanceof BubbleTextView) { + dragCondition = ((BubbleTextView) host).startLongPressAction(); + } else if (host instanceof BubbleTextHolder) { + BubbleTextHolder holder = (BubbleTextHolder) host; + dragCondition = holder.getBubbleText() == null ? null + : holder.getBubbleText().startLongPressAction(); } + return dragCondition != null; } else if (action == MOVE) { return beginAccessibleDrag(host, item, fromKeyboard); } else if (action == ADD_TO_WORKSPACE) { - final int[] coordinates = new int[2]; - final int screenId = findSpaceOnWorkspace(item, coordinates); - if (screenId == -1) { - return false; - } - mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { - if (item instanceof AppInfo) { - WorkspaceItemInfo info = ((AppInfo) item).makeWorkspaceItem(); - mLauncher.getModelWriter().addItemToDatabase(info, - Favorites.CONTAINER_DESKTOP, - screenId, coordinates[0], coordinates[1]); - - mLauncher.bindItems( - Collections.singletonList(info), - /* forceAnimateIcons= */ true, - /* focusFirstItemForAccessibility= */ true); - announceConfirmation(R.string.item_added_to_workspace); - } else if (item instanceof PendingAddItemInfo) { - PendingAddItemInfo info = (PendingAddItemInfo) item; - Workspace workspace = mLauncher.getWorkspace(); - workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); - mLauncher.addPendingItem(info, Favorites.CONTAINER_DESKTOP, - screenId, coordinates, info.spanX, info.spanY); - } - else if (item instanceof WorkspaceItemInfo) { - WorkspaceItemInfo info = ((WorkspaceItemInfo) item).clone(); - mLauncher.getModelWriter().addItemToDatabase(info, - Favorites.CONTAINER_DESKTOP, - screenId, coordinates[0], coordinates[1]); - mLauncher.bindItems(Collections.singletonList(info), true, true); - } - })); - return true; + return addToWorkspace(item, true); } else if (action == MOVE_TO_WORKSPACE) { - Folder folder = Folder.getOpen(mLauncher); - folder.close(true); - WorkspaceItemInfo info = (WorkspaceItemInfo) item; - folder.getInfo().remove(info, false); - - final int[] coordinates = new int[2]; - final int screenId = findSpaceOnWorkspace(item, coordinates); - if (screenId == -1) { - return false; - } - mLauncher.getModelWriter().moveItemInDatabase(info, - Favorites.CONTAINER_DESKTOP, - screenId, coordinates[0], coordinates[1]); - - // Bind the item in next frame so that if a new workspace page was created, - // it will get laid out. - new Handler().post(() -> { - mLauncher.bindItems(Collections.singletonList(item), true); - announceConfirmation(R.string.item_moved); - }); - return true; + return moveToWorkspace(item); } else if (action == RESIZE) { final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; List actions = getSupportedResizeActions(host, info); Rect pos = new Rect(); - mLauncher.getDragLayer().getDescendantRectRelativeToSelf(host, pos); - ArrowPopup popup = OptionsPopupView.show(mLauncher, new RectF(pos), actions, false); + mContext.getDragLayer().getDescendantRectRelativeToSelf(host, pos); + ArrowPopup popup = OptionsPopupView.show(mContext, new RectF(pos), actions, false); popup.requestFocus(); popup.setOnCloseCallback(host::requestFocus); return true; } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) { - return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null; + BubbleTextView btv = host instanceof BubbleTextView ? (BubbleTextView) host + : (host instanceof BubbleTextHolder + ? ((BubbleTextHolder) host).getBubbleText() : null); + return btv != null && PopupContainerWithArrow.showForIcon(btv) != null; } else { - for (ButtonDropTarget dropTarget : mLauncher.getDropTargetBar().getDropTargets()) { + for (ButtonDropTarget dropTarget : mContext.getDropTargetBar().getDropTargets()) { if (dropTarget.supportsAccessibilityDrop(item, host) && action == dropTarget.getAccessibilityAction()) { dropTarget.onAccessibilityDrop(host, item); @@ -315,7 +206,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { - actions.add(new OptionItem(mLauncher, + actions.add(new OptionItem(mContext, R.string.action_increase_width, R.drawable.ic_widget_width_increase, IGNORE, @@ -323,7 +214,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } if (info.spanX > info.minSpanX && info.spanX > 1) { - actions.add(new OptionItem(mLauncher, + actions.add(new OptionItem(mContext, R.string.action_decrease_width, R.drawable.ic_widget_width_decrease, IGNORE, @@ -334,7 +225,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { - actions.add(new OptionItem(mLauncher, + actions.add(new OptionItem(mContext, R.string.action_increase_height, R.drawable.ic_widget_height_increase, IGNORE, @@ -342,7 +233,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } if (info.spanY > info.minSpanY && info.spanY > 1) { - actions.add(new OptionItem(mLauncher, + actions.add(new OptionItem(mContext, R.string.action_decrease_height, R.drawable.ic_widget_height_decrease, IGNORE, @@ -382,58 +273,20 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } layout.markCellsAsOccupiedForView(host); - WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mLauncher, + WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mContext, info.spanX, info.spanY); host.requestLayout(); - mLauncher.getModelWriter().updateItemInDatabase(info); - announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY)); + mContext.getModelWriter().updateItemInDatabase(info); + announceConfirmation(mContext.getString(R.string.widget_resized, info.spanX, info.spanY)); return true; } @Thunk void announceConfirmation(int resId) { - announceConfirmation(mLauncher.getResources().getString(resId)); + announceConfirmation(mContext.getResources().getString(resId)); } - @Thunk void announceConfirmation(String confirmation) { - mLauncher.getDragLayer().announceForAccessibility(confirmation); - - } - - public boolean isInAccessibleDrag() { - return mDragInfo != null; - } - - public DragInfo getDragInfo() { - return mDragInfo; - } - - /** - * @param clickedTarget the actual view that was clicked - * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used - * as the actual drop location otherwise the views center is used. - */ - public void handleAccessibleDrop(View clickedTarget, Rect dropLocation, - String confirmation) { - if (!isInAccessibleDrag()) return; - - int[] loc = new int[2]; - if (dropLocation == null) { - loc[0] = clickedTarget.getWidth() / 2; - loc[1] = clickedTarget.getHeight() / 2; - } else { - loc[0] = dropLocation.centerX(); - loc[1] = dropLocation.centerY(); - } - - mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc); - mLauncher.getDragController().completeAccessibleDrag(loc); - - if (!TextUtils.isEmpty(confirmation)) { - announceConfirmation(confirmation); - } - } - - private boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) { + @Override + protected boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) { if (!itemSupportsAccessibleDrag(info)) { return false; } @@ -449,8 +302,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } Rect pos = new Rect(); - mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos); - mLauncher.getDragController().addDragListener(this); + mContext.getDragLayer().getDescendantRectRelativeToSelf(item, pos); + mContext.getDragController().addDragListener(this); DragOptions options = new DragOptions(); options.isAccessibleDrag = true; @@ -458,31 +311,20 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY()); if (fromKeyboard) { - KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mLauncher.getLayoutInflater() - .inflate(R.layout.keyboard_drag_and_drop, mLauncher.getDragLayer(), false); + KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mContext.getLayoutInflater() + .inflate(R.layout.keyboard_drag_and_drop, mContext.getDragLayer(), false); popup.showForIcon(item, info, options); } else { - ItemLongClickListener.beginDrag(item, mLauncher, info, options); + ItemLongClickListener.beginDrag(item, mContext, info, options); } return true; } - @Override - public void onDragStart(DragObject dragObject, DragOptions options) { - // No-op - } - - @Override - public void onDragEnd() { - mLauncher.getDragController().removeDragListener(this); - mDragInfo = null; - } - /** * Find empty space on the workspace and returns the screenId. */ protected int findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { - Workspace workspace = mLauncher.getWorkspace(); + Workspace workspace = mContext.getWorkspace(); IntArray workspaceScreens = workspace.getScreenOrder(); int screenId; @@ -521,28 +363,75 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return screenId; } - public class LauncherAction { - public final int keyCode; - public final AccessibilityAction accessibilityAction; - - private final LauncherAccessibilityDelegate mDelegate; - - public LauncherAction(int id, int labelRes, int keyCode) { - this.keyCode = keyCode; - accessibilityAction = new AccessibilityAction(id, mLauncher.getString(labelRes)); - mDelegate = LauncherAccessibilityDelegate.this; + /** + * Functionality to add the item {@link ItemInfo} to the workspace + * @param item item to be added + * @param accessibility true if the first item to be added to the workspace + * should be focused for accessibility. + * + * @return true if the item could be successfully added + */ + public boolean addToWorkspace(ItemInfo item, boolean accessibility) { + final int[] coordinates = new int[2]; + final int screenId = findSpaceOnWorkspace(item, coordinates); + if (screenId == -1) { + return false; } + mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { + if (item instanceof WorkspaceItemFactory) { + WorkspaceItemInfo info = ((WorkspaceItemFactory) item).makeWorkspaceItem(mContext); + mContext.getModelWriter().addItemToDatabase(info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); - /** - * Invokes the action for the provided host - */ - public boolean invokeFromKeyboard(View host) { - if (host != null && host.getTag() instanceof ItemInfo) { - return mDelegate.performAction( - host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true); - } else { - return false; + mContext.bindItems( + Collections.singletonList(info), + /* forceAnimateIcons= */ true, + /* focusFirstItemForAccessibility= */ accessibility); + announceConfirmation(R.string.item_added_to_workspace); + } else if (item instanceof PendingAddItemInfo) { + PendingAddItemInfo info = (PendingAddItemInfo) item; + Workspace workspace = mContext.getWorkspace(); + workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); + mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates, info.spanX, info.spanY); + } else if (item instanceof WorkspaceItemInfo) { + WorkspaceItemInfo info = ((WorkspaceItemInfo) item).clone(); + mContext.getModelWriter().addItemToDatabase(info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + mContext.bindItems(Collections.singletonList(info), true, accessibility); } + })); + return true; + } + /** + * Functionality to move the item {@link ItemInfo} to the workspace + * @param item item to be moved + * + * @return true if the item could be successfully added + */ + public boolean moveToWorkspace(ItemInfo item) { + Folder folder = Folder.getOpen(mContext); + folder.close(true); + WorkspaceItemInfo info = (WorkspaceItemInfo) item; + folder.getInfo().remove(info, false); + + final int[] coordinates = new int[2]; + final int screenId = findSpaceOnWorkspace(item, coordinates); + if (screenId == -1) { + return false; } + mContext.getModelWriter().moveItemInDatabase(info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + + // Bind the item in next frame so that if a new workspace page was created, + // it will get laid out. + new Handler().post(() -> { + mContext.bindItems(Collections.singletonList(item), true); + announceConfirmation(R.string.item_moved); + }); + return true; } } diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index bf5a24b65b..fb847ec9ae 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -71,12 +71,12 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele if (screenId == -1) { return false; } - mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { - mLauncher.getModelWriter().addItemToDatabase(info, + mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { + mContext.getModelWriter().addItemToDatabase(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); - mLauncher.bindItems(Collections.singletonList(info), true); - AbstractFloatingView.closeAllOpenViews(mLauncher); + mContext.bindItems(Collections.singletonList(info), true); + AbstractFloatingView.closeAllOpenViews(mContext); announceConfirmation(R.string.item_added_to_workspace); })); return true; diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index a331924f22..a8624dd17d 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -22,7 +22,7 @@ import android.view.View; import com.android.launcher3.CellLayout; import com.android.launcher3.R; -import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType; +import com.android.launcher3.accessibility.BaseAccessibilityDelegate.DragType; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java new file mode 100644 index 0000000000..53a6fd7edb --- /dev/null +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.RelativeLayout; + +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.launcher3.DeviceProfile.DeviceProfileListenable; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; +import com.android.launcher3.allapps.search.SearchAdapterProvider; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.views.AppLauncher; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * All apps container view with search support for use in a dragging activity. + * + * @param Type of context inflating all apps. + */ +public class ActivityAllAppsContainerView extends BaseAllAppsContainerView { + + protected SearchUiManager mSearchUiManager; + /** + * View that defines the search box. Result is rendered inside the recycler view defined in the + * base class. + */ + private View mSearchContainer; + /** {@code true} when rendered view is in search state instead of the scroll state. */ + private boolean mIsSearching; + + public ActivityAllAppsContainerView(Context context) { + this(context, null); + } + + public ActivityAllAppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SearchUiManager getSearchUiManager() { + return mSearchUiManager; + } + + public View getSearchView() { + return mSearchContainer; + } + + /** Updates all apps container with the latest search query. */ + public void setLastSearchQuery(String query) { + Intent marketSearchIntent = PackageManagerHelper.getMarketSearchIntent( + mActivityContext, query); + OnClickListener marketSearchClickListener = (v) -> mActivityContext.startActivitySafely(v, + marketSearchIntent, null); + for (int i = 0; i < mAH.size(); i++) { + mAH.get(i).mAdapter.setLastSearchQuery(query, marketSearchClickListener); + } + mIsSearching = true; + rebindAdapters(); + mHeader.setCollapsed(true); + } + + /** Invoke when the current search session is finished. */ + public void onClearSearchResult() { + mIsSearching = false; + mHeader.setCollapsed(false); + rebindAdapters(); + mHeader.reset(false); + } + + /** + * Sets results list for search + */ + public void setSearchResults(ArrayList results) { + if (getSearchResultList().setSearchResults(results)) { + for (int i = 0; i < mAH.size(); i++) { + if (mAH.get(i).mRecyclerView != null) { + mAH.get(i).mRecyclerView.onSearchResultsChanged(); + } + } + } + } + + @Override + protected final SearchAdapterProvider createMainAdapterProvider() { + return mActivityContext.createSearchAdapterProvider(this); + } + + @Override + public boolean shouldContainerScroll(MotionEvent ev) { + // IF the MotionEvent is inside the search box, and the container keeps on receiving + // touch input, container should move down. + if (mActivityContext.getDragLayer().isEventOverView(mSearchContainer, ev)) { + return true; + } + return super.shouldContainerScroll(ev); + } + + @Override + public void reset(boolean animate) { + super.reset(animate); + // Reset the search bar after transitioning home. + mSearchUiManager.resetSearch(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSearchContainer = findViewById(R.id.search_container_all_apps); + mSearchUiManager = (SearchUiManager) mSearchContainer; + mSearchUiManager.initializeSearch(this); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + mSearchUiManager.preDispatchKeyEvent(event); + return super.dispatchKeyEvent(event); + } + + @Override + public String getDescription() { + if (!mUsingTabs && isSearching()) { + return getContext().getString(R.string.all_apps_search_results); + } else { + return super.getDescription(); + } + } + + @Override + protected boolean shouldShowTabs() { + return super.shouldShowTabs() && !isSearching(); + } + + @Override + public boolean isSearching() { + return mIsSearching; + } + + @Override + protected void rebindAdapters(boolean force) { + super.rebindAdapters(force); + if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get() + || getMainAdapterProvider().getDecorator() == null) { + return; + } + + RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator(); + mAH.stream() + .map(adapterHolder -> adapterHolder.mRecyclerView) + .filter(Objects::nonNull) + .forEach(v -> { + v.removeItemDecoration(decoration); // Remove in case it is already added. + v.addItemDecoration(decoration); + }); + } + + @Override + protected View replaceAppsRVContainer(boolean showTabs) { + View rvContainer = super.replaceAppsRVContainer(showTabs); + + removeCustomRules(rvContainer); + removeCustomRules(getSearchRecyclerView()); + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + alignParentTop(rvContainer, showTabs); + alignParentTop(getSearchRecyclerView(), /* tabs= */ false); + layoutAboveSearchContainer(rvContainer); + layoutAboveSearchContainer(getSearchRecyclerView()); + } else { + layoutBelowSearchContainer(rvContainer, showTabs); + layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false); + } + + return rvContainer; + } + + @Override + void setupHeader() { + super.setupHeader(); + + removeCustomRules(mHeader); + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + alignParentTop(mHeader, false /* includeTabsMargin */); + } else { + layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); + } + } + + @Override + protected void updateHeaderScroll(int scrolledOffset) { + super.updateHeaderScroll(scrolledOffset); + if (mSearchUiManager.getEditText() == null) { + return; + } + + float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); + boolean bgVisible = mSearchUiManager.getBackgroundVisibility(); + if (scrolledOffset == 0 && !isSearching()) { + bgVisible = true; + } else if (scrolledOffset > mHeaderThreshold) { + bgVisible = false; + } + mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); + } + + @Override + protected int getHeaderColor(float blendRatio) { + return ColorUtils.setAlphaComponent( + super.getHeaderColor(blendRatio), + (int) (mSearchContainer.getAlpha() * 255)); + } + + @Override + public int getHeaderBottom() { + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + return super.getHeaderBottom(); + } + return super.getHeaderBottom() + mSearchContainer.getBottom(); + } + + private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) { + if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { + return; + } + + RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); + layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps); + + int topMargin = getContext().getResources().getDimensionPixelSize( + R.dimen.all_apps_header_top_margin); + if (includeTabsMargin) { + topMargin += getContext().getResources().getDimensionPixelSize( + R.dimen.all_apps_header_pill_height); + } + layoutParams.topMargin = topMargin; + } + + private void layoutAboveSearchContainer(View v) { + if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { + return; + } + + RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); + layoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps); + } + + private void alignParentTop(View v, boolean includeTabsMargin) { + if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { + return; + } + + RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.topMargin = + includeTabsMargin + ? getContext().getResources().getDimensionPixelSize( + R.dimen.all_apps_header_pill_height) + : 0; + } + + private void removeCustomRules(View v) { + if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { + return; + } + + RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); + layoutParams.removeRule(RelativeLayout.ABOVE); + layoutParams.removeRule(RelativeLayout.ALIGN_TOP); + layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP); + } + + @Override + protected BaseAllAppsAdapter createAdapter(AlphabeticalAppsList appsList, + BaseAdapterProvider[] adapterProviders) { + return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, + adapterProviders); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index f97eb28dda..7067fa225b 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -37,10 +37,10 @@ public class AllAppsFastScrollHelper { * Smooth scrolls the recycler view to the given section. */ public void smoothScrollToSection(FastScrollSectionInfo info) { - if (mTargetFastScrollPosition == info.fastScrollToItem.position) { + if (mTargetFastScrollPosition == info.position) { return; } - mTargetFastScrollPosition = info.fastScrollToItem.position; + mTargetFastScrollPosition = info.position; mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition)); } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index d5c9a53f12..33d2f2b1df 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -15,149 +15,48 @@ */ package com.android.launcher3.allapps; -import static com.android.launcher3.touch.ItemLongClickListener.INSTANCE_ALL_APPS; - import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnFocusChangeListener; -import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.view.accessibility.AccessibilityEventCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityRecordCompat; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.BubbleTextView; -import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.views.ActivityContext; -import java.util.Arrays; import java.util.List; /** * The grid view adapter of all the apps. + * + * @param Type of context inflating all apps. */ -public class AllAppsGridAdapter extends - RecyclerView.Adapter { +public class AllAppsGridAdapter extends + BaseAllAppsAdapter { public static final String TAG = "AppsGridAdapter"; + private final GridLayoutManager mGridLayoutMgr; + private final GridSpanSizer mGridSizer; - // A normal icon - public static final int VIEW_TYPE_ICON = 1 << 1; - // The message shown when there are no filtered results - public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2; - // The message to continue to a market search when there are no filtered results - public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3; - - // We use various dividers for various purposes. They share enough attributes to reuse layouts, - // but differ in enough attributes to require different view types - - // A divider that separates the apps list and the search market button - public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4; - - // Common view type masks - public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; - public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; - - - private final BaseAdapterProvider[] mAdapterProviders; - - /** - * ViewHolder for each icon. - */ - public static class ViewHolder extends RecyclerView.ViewHolder { - - public ViewHolder(View v) { - super(v); - } + public AllAppsGridAdapter(T activityContext, LayoutInflater inflater, + AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) { + super(activityContext, inflater, apps, adapterProviders); + mGridSizer = new GridSpanSizer(); + mGridLayoutMgr = new AppsGridLayoutManager(mActivityContext); + mGridLayoutMgr.setSpanSizeLookup(mGridSizer); + setAppsPerRow(activityContext.getDeviceProfile().numShownAllAppsColumns); } /** - * Info about a particular adapter item (can be either section or app) + * Returns the grid layout manager. */ - public static class AdapterItem { - /** Common properties */ - // The index of this adapter item in the list - public int position; - // The type of this item - public int viewType; - - // The section name of this item. Note that there can be multiple items with different - // sectionNames in the same section - public String sectionName = null; - // The row that this item shows up on - public int rowIndex; - // The index of this app in the row - public int rowAppIndex; - // The associated ItemInfoWithIcon for the item - public ItemInfoWithIcon itemInfo = null; - // The index of this app not including sections - public int appIndex = -1; - // Search section associated to result - public DecorationInfo decorationInfo = null; - - /** - * Factory method for AppIcon AdapterItem - */ - public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, - int appIndex) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_ICON; - item.position = pos; - item.sectionName = sectionName; - item.itemInfo = appInfo; - item.appIndex = appIndex; - return item; - } - - /** - * Factory method for empty search results view - */ - public static AdapterItem asEmptySearch(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_EMPTY_SEARCH; - item.position = pos; - return item; - } - - /** - * Factory method for a dividerView in AllAppsSearch - */ - public static AdapterItem asAllAppsDivider(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER; - item.position = pos; - return item; - } - - /** - * Factory method for a market search button - */ - public static AdapterItem asMarketSearch(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_SEARCH_MARKET; - item.position = pos; - return item; - } - - protected boolean isCountedForAccessibility() { - return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET; - } + public RecyclerView.LayoutManager getLayoutManager() { + return mGridLayoutMgr; } /** @@ -217,9 +116,9 @@ public class AllAppsGridAdapter extends */ private int getRowsNotForAccessibility(int adapterPosition) { List items = mApps.getAdapterItems(); - adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); + adapterPosition = Math.max(adapterPosition, items.size() - 1); int extraRows = 0; - for (int i = 0; i <= adapterPosition; i++) { + for (int i = 0; i <= adapterPosition && i < items.size(); i++) { if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_ICON)) { extraRows++; } @@ -228,6 +127,20 @@ public class AllAppsGridAdapter extends } } + @Override + public void setAppsPerRow(int appsPerRow) { + mAppsPerRow = appsPerRow; + int totalSpans = mAppsPerRow; + for (BaseAdapterProvider adapterProvider : mAdapterProviders) { + for (int itemPerRow : adapterProvider.getSupportedItemsPerRowArray()) { + if (totalSpans % itemPerRow != 0) { + totalSpans *= itemPerRow; + } + } + } + mGridLayoutMgr.setSpanCount(totalSpans); + } + /** * Helper class to size the grid items. */ @@ -255,201 +168,4 @@ public class AllAppsGridAdapter extends } } } - - private final BaseDraggingActivity mLauncher; - private final LayoutInflater mLayoutInflater; - private final AlphabeticalAppsList mApps; - private final GridLayoutManager mGridLayoutMgr; - private final GridSpanSizer mGridSizer; - - private final OnClickListener mOnIconClickListener; - private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS; - - private int mAppsPerRow; - - private OnFocusChangeListener mIconFocusListener; - - // The text to show when there are no search results and no market search handler. - protected String mEmptySearchMessage; - // The intent to send off to the market app, updated each time the search query changes. - private Intent mMarketSearchIntent; - - private final int mExtraHeight; - - public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, - AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) { - Resources res = launcher.getResources(); - mLauncher = launcher; - mApps = apps; - mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); - mGridSizer = new GridSpanSizer(); - mGridLayoutMgr = new AppsGridLayoutManager(launcher); - mGridLayoutMgr.setSpanSizeLookup(mGridSizer); - mLayoutInflater = inflater; - - mOnIconClickListener = launcher.getItemOnClickListener(); - - mAdapterProviders = adapterProviders; - setAppsPerRow(mLauncher.getDeviceProfile().numShownAllAppsColumns); - mExtraHeight = launcher.getResources().getDimensionPixelSize(R.dimen.all_apps_height_extra); - } - - public void setAppsPerRow(int appsPerRow) { - mAppsPerRow = appsPerRow; - int totalSpans = mAppsPerRow; - for (BaseAdapterProvider adapterProvider : mAdapterProviders) { - for (int itemPerRow : adapterProvider.getSupportedItemsPerRowArray()) { - if (totalSpans % itemPerRow != 0) { - totalSpans *= itemPerRow; - } - } - } - mGridLayoutMgr.setSpanCount(totalSpans); - } - - /** - * Sets the long click listener for icons - */ - public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) { - mOnIconLongClickListener = listener; - } - - public static boolean isDividerViewType(int viewType) { - return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); - } - - public static boolean isIconViewType(int viewType) { - return isViewType(viewType, VIEW_TYPE_MASK_ICON); - } - - public static boolean isViewType(int viewType, int viewTypeMask) { - return (viewType & viewTypeMask) != 0; - } - - public void setIconFocusListener(OnFocusChangeListener focusListener) { - mIconFocusListener = focusListener; - } - - /** - * Sets the last search query that was made, used to show when there are no results and to also - * seed the intent for searching the market. - */ - public void setLastSearchQuery(String query) { - Resources res = mLauncher.getResources(); - mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); - mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); - } - - /** - * Returns the grid layout manager. - */ - public GridLayoutManager getLayoutManager() { - return mGridLayoutMgr; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_TYPE_ICON: - int layout = !FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() ? R.layout.all_apps_icon - : R.layout.all_apps_icon_twoline; - BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( - layout, parent, false); - icon.setLongPressTimeoutFactor(1f); - icon.setOnFocusChangeListener(mIconFocusListener); - icon.setOnClickListener(mOnIconClickListener); - icon.setOnLongClickListener(mOnIconLongClickListener); - // Ensure the all apps icon height matches the workspace icons in portrait mode. - icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; - if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) { - icon.getLayoutParams().height += mExtraHeight; - } - return new ViewHolder(icon); - case VIEW_TYPE_EMPTY_SEARCH: - return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, - parent, false)); - case VIEW_TYPE_SEARCH_MARKET: - View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, - parent, false); - searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely( - v, mMarketSearchIntent, null)); - return new ViewHolder(searchMarketView); - case VIEW_TYPE_ALL_APPS_DIVIDER: - return new ViewHolder(mLayoutInflater.inflate( - R.layout.all_apps_divider, parent, false)); - default: - BaseAdapterProvider adapterProvider = getAdapterProvider(viewType); - if (adapterProvider != null) { - return adapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType); - } - throw new RuntimeException("Unexpected view type" + viewType); - } - } - - @Override - public void onBindViewHolder(ViewHolder holder, int position) { - switch (holder.getItemViewType()) { - case VIEW_TYPE_ICON: - AdapterItem adapterItem = mApps.getAdapterItems().get(position); - BubbleTextView icon = (BubbleTextView) holder.itemView; - icon.reset(); - if (adapterItem.itemInfo instanceof AppInfo) { - icon.applyFromApplicationInfo((AppInfo) adapterItem.itemInfo); - } else { - icon.applyFromItemInfoWithIcon(adapterItem.itemInfo); - } - break; - case VIEW_TYPE_EMPTY_SEARCH: - TextView emptyViewText = (TextView) holder.itemView; - emptyViewText.setText(mEmptySearchMessage); - emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : - Gravity.START | Gravity.CENTER_VERTICAL); - break; - case VIEW_TYPE_SEARCH_MARKET: - TextView searchView = (TextView) holder.itemView; - if (mMarketSearchIntent != null) { - searchView.setVisibility(View.VISIBLE); - } else { - searchView.setVisibility(View.GONE); - } - break; - case VIEW_TYPE_ALL_APPS_DIVIDER: - // nothing to do - break; - default: - BaseAdapterProvider adapterProvider = getAdapterProvider(holder.getItemViewType()); - if (adapterProvider != null) { - adapterProvider.onBindView(holder, position); - } - } - } - - @Override - public void onViewRecycled(@NonNull ViewHolder holder) { - super.onViewRecycled(holder); - } - - @Override - public boolean onFailedToRecycleView(ViewHolder holder) { - // Always recycle and we will reset the view when it is bound - return true; - } - - @Override - public int getItemCount() { - return mApps.getAdapterItems().size(); - } - - @Override - public int getItemViewType(int position) { - AdapterItem item = mApps.getAdapterItems().get(position); - return item.viewType; - } - - @Nullable - private BaseAdapterProvider getAdapterProvider(int viewType) { - return Arrays.stream(mAdapterProviders).filter( - adapterProvider -> adapterProvider.isViewSupported(viewType)).findFirst().orElse( - null); - } } diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java index 3cc9ce6806..872503ae1c 100644 --- a/src/com/android/launcher3/allapps/AllAppsPagedView.java +++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java @@ -21,13 +21,13 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import android.content.Context; import android.util.AttributeSet; -import com.android.launcher3.Launcher; import com.android.launcher3.PagedView; +import com.android.launcher3.views.ActivityContext; import com.android.launcher3.workprofile.PersonalWorkPagedView; /** - * A {@link PagedView} for showing different views for the personal and work profile respectively - * in the {@link AllAppsContainerView}. + * A {@link PagedView} for showing different views for the personal and work profile respectively + * in the {@link BaseAllAppsContainerView}. */ public class AllAppsPagedView extends PersonalWorkPagedView { @@ -47,7 +47,7 @@ public class AllAppsPagedView extends PersonalWorkPagedView { protected boolean snapToPageWithVelocity(int whichPage, int velocity) { boolean resp = super.snapToPageWithVelocity(whichPage, velocity); if (resp && whichPage != mCurrentPage) { - Launcher.getLauncher(getContext()).getStatsLogManager().logger() + ActivityContext.lookupContext(getContext()).getStatsLogManager().logger() .log(mCurrentPage < whichPage ? LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB : LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB); diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index bccd9b41a0..af17cf72e9 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,10 +15,9 @@ */ package com.android.launcher3.allapps; -import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.UNSPECIFIED; -import static android.view.View.MeasureSpec.makeMeasureSpec; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SCROLLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END; import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING; @@ -36,9 +35,8 @@ import android.view.View; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.FastScrollRecyclerView; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -47,19 +45,18 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.RecyclerViewFastScroller; -import java.util.ArrayList; import java.util.List; /** * A RecyclerView with custom fast scroll support for the all apps view. */ -public class AllAppsRecyclerView extends BaseRecyclerView { - private static final String TAG = "AllAppsContainerView"; +public class AllAppsRecyclerView extends FastScrollRecyclerView { + protected static final String TAG = "AllAppsRecyclerView"; private static final boolean DEBUG = false; private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING); - private AlphabeticalAppsList mApps; - private final int mNumAppsPerRow; + protected AlphabeticalAppsList mApps; + protected final int mNumAppsPerRow; // The specific view heights that we use to calculate scroll private final SparseIntArray mViewHeights = new SparseIntArray(); @@ -71,13 +68,31 @@ public class AllAppsRecyclerView extends BaseRecyclerView { public void onChanged() { mCachedScrollPositions.clear(); } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + onChanged(); + } }; // The empty-search result background - private AllAppsBackgroundDrawable mEmptySearchBackground; - private int mEmptySearchBackgroundTopOffset; - - private ArrayList mAutoSizedOverlays = new ArrayList<>(); + protected AllAppsBackgroundDrawable mEmptySearchBackground; + protected int mEmptySearchBackgroundTopOffset; public AllAppsRecyclerView(Context context) { this(context, null); @@ -104,16 +119,16 @@ public class AllAppsRecyclerView extends BaseRecyclerView { /** * Sets the list of apps in this view, used to determine the fastscroll position. */ - public void setApps(AlphabeticalAppsList apps) { + public void setApps(AlphabeticalAppsList apps) { mApps = apps; } - public AlphabeticalAppsList getApps() { + public AlphabeticalAppsList getApps() { return mApps; } - private void updatePoolSize() { - DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile(); + protected void updatePoolSize() { + DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); RecyclerView.RecycledViewPool pool = getRecycledViewPool(); int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); @@ -137,8 +152,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView { Log.d(TAG, "onDraw at = " + System.currentTimeMillis()); } if (DEBUG_LATENCY) { - Log.d(SEARCH_LOGGING, - "-- Recycle view onDraw, time stamp = " + System.currentTimeMillis()); + Log.d(SEARCH_LOGGING, getClass().getSimpleName() + " onDraw; time stamp = " + + System.currentTimeMillis()); } super.onDraw(c); } @@ -152,30 +167,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView { protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateEmptySearchBackgroundBounds(); updatePoolSize(); - for (int i = 0; i < mAutoSizedOverlays.size(); i++) { - View overlay = mAutoSizedOverlays.get(i); - overlay.measure(makeMeasureSpec(w, EXACTLY), makeMeasureSpec(w, EXACTLY)); - overlay.layout(0, 0, w, h); - } - } - - /** - * Adds an overlay that automatically rescales with the recyclerview. - */ - public void addAutoSizedOverlay(View overlay) { - mAutoSizedOverlays.add(overlay); - getOverlay().add(overlay); - onSizeChanged(getWidth(), getHeight(), getWidth(), getHeight()); - } - - /** - * Clears auto scaling overlay views added by #addAutoSizedOverlay - */ - public void clearAutoSizedOverlays() { - for (View v : mAutoSizedOverlays) { - getOverlay().remove(v); - } - mAutoSizedOverlays.clear(); } public void onSearchResultsChanged() { @@ -201,12 +192,15 @@ public class AllAppsRecyclerView extends BaseRecyclerView { public void onScrollStateChanged(int state) { super.onScrollStateChanged(state); - StatsLogManager mgr = BaseDraggingActivity.fromContext(getContext()).getStatsLogManager(); + StatsLogManager mgr = ActivityContext.lookupContext(getContext()).getStatsLogManager(); switch (state) { case SCROLL_STATE_DRAGGING: + mgr.logger().log(LAUNCHER_ALLAPPS_SCROLLED); requestFocus(); mgr.logger().sendToInteractionJankMonitor( LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this); + hideKeyboardAsync(ActivityContext.lookupContext(getContext()), + getApplicationWindowToken()); break; case SCROLL_STATE_IDLE: mgr.logger().sendToInteractionJankMonitor( @@ -222,8 +216,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView { && mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { mEmptySearchBackground.setHotspot(e.getX(), e.getY()); } - hideKeyboardAsync(ActivityContext.lookupContext(getContext()), - getApplicationWindowToken()); return result; } @@ -240,17 +232,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView { // Find the fastscroll section that maps to this touch fraction List fastScrollSections = mApps.getFastScrollerSections(); - AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); - for (int i = 1; i < fastScrollSections.size(); i++) { - AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); - if (info.touchFraction > touchFraction) { - break; - } - lastInfo = info; + int count = fastScrollSections.size(); + if (count == 0) { + return ""; } - - mFastScrollHelper.smoothScrollToSection(lastInfo); - return lastInfo.sectionName; + int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1); + AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index); + mFastScrollHelper.smoothScrollToSection(section); + return section.sectionName; } @Override @@ -270,12 +259,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView { } } - @Override - protected float getBottomFadingEdgeStrength() { - // No bottom fading edge. - return 0; - } - @Override protected boolean isPaddingOffsetRequired() { return true; @@ -358,13 +341,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView { } } - @Override - public boolean supportsFastScrolling() { - // Only allow fast scrolling when the user is not searching, since the results are not - // grouped in a meaningful order - return !mApps.hasFilter(); - } - @Override public int getCurrentScrollY() { // Return early if there are no items or we haven't been measured @@ -375,7 +351,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView { // Calculate the y and offset for the item View child = getChildAt(0); - int position = getChildPosition(child); + int position = getChildAdapterPosition(child); if (position == NO_POSITION) { return -1; } @@ -465,14 +441,4 @@ public class AllAppsRecyclerView extends BaseRecyclerView { public boolean hasOverlappingRendering() { return false; } - - /** - * Returns distance between left and right app icons - */ - public int getTabWidth() { - DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile(); - int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - int iconPadding = totalWidth / grid.numShownAllAppsColumns - grid.allAppsIconSizePx; - return totalWidth - iconPadding - grid.allAppsIconDrawablePaddingPx; - } } diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index fc78beae58..a4a208533a 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -44,6 +44,9 @@ import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.util.MultiAdditivePropertyFactory; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.views.ScrimView; /** @@ -75,7 +78,57 @@ public class AllAppsTransitionController } }; - private AllAppsContainerView mAppsView; + public static final FloatProperty ALL_APPS_PULL_BACK_TRANSLATION = + new FloatProperty("allAppsPullBackTranslation") { + + @Override + public Float get(AllAppsTransitionController controller) { + if (controller.mIsTablet) { + return controller.mAppsView.getActiveRecyclerView().getTranslationY(); + } else { + return controller.getAppsViewPullbackTranslationY().get( + controller.mAppsView); + } + } + + @Override + public void setValue(AllAppsTransitionController controller, float translation) { + if (controller.mIsTablet) { + controller.mAppsView.getActiveRecyclerView().setTranslationY(translation); + } else { + controller.getAppsViewPullbackTranslationY().set(controller.mAppsView, + translation); + } + } + }; + + public static final FloatProperty ALL_APPS_PULL_BACK_ALPHA = + new FloatProperty("allAppsPullBackAlpha") { + + @Override + public Float get(AllAppsTransitionController controller) { + if (controller.mIsTablet) { + return controller.mAppsView.getActiveRecyclerView().getAlpha(); + } else { + return controller.getAppsViewPullbackAlpha().getValue(); + } + } + + @Override + public void setValue(AllAppsTransitionController controller, float alpha) { + if (controller.mIsTablet) { + controller.mAppsView.getActiveRecyclerView().setAlpha(alpha); + } else { + controller.getAppsViewPullbackAlpha().setValue(alpha); + } + } + }; + + private static final int INDEX_APPS_VIEW_PROGRESS = 0; + private static final int INDEX_APPS_VIEW_PULLBACK = 1; + private static final int APPS_VIEW_INDEX_COUNT = 2; + + private ActivityAllAppsContainerView mAppsView; private final Launcher mLauncher; private boolean mIsVerticalLayout; @@ -89,15 +142,22 @@ public class AllAppsTransitionController private float mShiftRange; // changes depending on the orientation private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent - private float mScrollRangeDelta = 0; private ScrimView mScrimView; + private final MultiAdditivePropertyFactory + mAppsViewTranslationYPropertyFactory = new MultiAdditivePropertyFactory<>( + "appsViewTranslationY", View.TRANSLATION_Y); + private MultiValueAlpha mAppsViewAlpha; + + private boolean mIsTablet; + public AllAppsTransitionController(Launcher l) { mLauncher = l; - mShiftRange = mLauncher.getDeviceProfile().heightPx; + DeviceProfile dp = mLauncher.getDeviceProfile(); + setShiftRange(dp.allAppsShiftRange); mProgress = 1f; - - mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout(); + mIsVerticalLayout = dp.isVerticalBarLayout(); + mIsTablet = dp.isTablet; mLauncher.addOnDeviceProfileChangeListener(this); } @@ -108,12 +168,14 @@ public class AllAppsTransitionController @Override public void onDeviceProfileChanged(DeviceProfile dp) { mIsVerticalLayout = dp.isVerticalBarLayout(); - setScrollRangeDelta(mScrollRangeDelta); + setShiftRange(dp.allAppsShiftRange); if (mIsVerticalLayout) { mLauncher.getHotseat().setTranslationY(0); mLauncher.getWorkspace().getPageIndicator().setTranslationY(0); } + + mIsTablet = dp.isTablet; } /** @@ -126,13 +188,30 @@ public class AllAppsTransitionController */ public void setProgress(float progress) { mProgress = progress; - mAppsView.setTranslationY(mProgress * mShiftRange); + getAppsViewProgressTranslationY().set(mAppsView, mProgress * mShiftRange); + mLauncher.onAllAppsTransition(1 - progress); } public float getProgress() { return mProgress; } + private FloatProperty getAppsViewProgressTranslationY() { + return mAppsViewTranslationYPropertyFactory.get(INDEX_APPS_VIEW_PROGRESS); + } + + private FloatProperty getAppsViewPullbackTranslationY() { + return mAppsViewTranslationYPropertyFactory.get(INDEX_APPS_VIEW_PULLBACK); + } + + private MultiValueAlpha.AlphaProperty getAppsViewProgressAlpha() { + return mAppsViewAlpha.getProperty(INDEX_APPS_VIEW_PROGRESS); + } + + private MultiValueAlpha.AlphaProperty getAppsViewPullbackAlpha() { + return mAppsViewAlpha.getProperty(INDEX_APPS_VIEW_PULLBACK); + } + /** * Sets the vertical transition progress to {@param state} and updates all the dependent UI * accordingly. @@ -151,6 +230,15 @@ public class AllAppsTransitionController @Override public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, PendingAnimation builder) { + if (NORMAL.equals(toState) && mLauncher.isInState(ALL_APPS)) { + UiThreadHelper.hideKeyboardAsync(mLauncher, mLauncher.getAppsView().getWindowToken()); + builder.addEndListener(success -> { + // Reset pull back progress and alpha after switching states. + ALL_APPS_PULL_BACK_TRANSLATION.set(this, 0f); + ALL_APPS_PULL_BACK_ALPHA.set(this, 1f); + }); + } + float targetProgress = toState.getVerticalProgress(mLauncher); if (Float.compare(mProgress, targetProgress) == 0) { setAlphas(toState, config, builder); @@ -160,10 +248,10 @@ public class AllAppsTransitionController } // need to decide depending on the release velocity - Interpolator interpolator = (config.userControlled ? LINEAR : DEACCEL_1_7); - + Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS, + config.userControlled ? LINEAR : DEACCEL_1_7); Animator anim = createSpringAnimation(mProgress, targetProgress); - anim.setInterpolator(config.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator)); + anim.setInterpolator(verticalProgressInterpolator); anim.addListener(getProgressAnimatorListener()); builder.add(anim); @@ -187,7 +275,8 @@ public class AllAppsTransitionController boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0; Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR); - setter.setViewAlpha(mAppsView, hasAllAppsContent ? 1 : 0, allAppsFade); + setter.setFloat(getAppsViewProgressAlpha(), MultiValueAlpha.VALUE, + hasAllAppsContent ? 1 : 0, allAppsFade); boolean shouldProtectHeader = ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS; @@ -201,7 +290,7 @@ public class AllAppsTransitionController /** * see Launcher#setupViews */ - public void setupViews(ScrimView scrimView, AllAppsContainerView appsView) { + public void setupViews(ScrimView scrimView, ActivityAllAppsContainerView appsView) { mScrimView = scrimView; mAppsView = appsView; if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && Utilities.ATLEAST_R) { @@ -210,14 +299,15 @@ public class AllAppsTransitionController | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); } mAppsView.setScrimView(scrimView); + mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT); + mAppsViewAlpha.setUpdateVisibility(true); } /** * Updates the total scroll range but does not update the UI. */ - public void setScrollRangeDelta(float delta) { - mScrollRangeDelta = delta; - mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta; + public void setShiftRange(float shiftRange) { + mShiftRange = shiftRange; } /** diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index ce5c5890ea..45a567dd19 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -15,35 +15,41 @@ */ package com.android.launcher3.allapps; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ALL_APPS_DIVIDER; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_EMPTY_SEARCH; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_SEARCH_MARKET; import android.content.Context; -import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.LabelComparator; +import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * The alphabetically sorted list of applications. + * + * @param Type of context inflating this view. */ -public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { +public class AlphabeticalAppsList implements + AllAppsStore.OnUpdateListener { public static final String TAG = "AlphabeticalAppsList"; - private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; - private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; - - private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; private final WorkAdapterProvider mWorkAdapterProvider; /** @@ -52,22 +58,22 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { */ public static class FastScrollSectionInfo { // The section name - public String sectionName; - // The AdapterItem to scroll to for this section - public AdapterItem fastScrollToItem; - // The touch fraction that should map to this fast scroll section info - public float touchFraction; + public final String sectionName; + // The item position + public final int position; - public FastScrollSectionInfo(String sectionName) { + public FastScrollSectionInfo(String sectionName, int position) { this.sectionName = sectionName; + this.position = position; } } - private final BaseDraggingActivity mLauncher; + private final T mActivityContext; // The set of apps from the system private final List mApps = new ArrayList<>(); + @Nullable private final AllAppsStore mAllAppsStore; // The number of results in current adapter @@ -78,24 +84,26 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { private final List mFastScrollerSections = new ArrayList<>(); // The of ordered component names as a result of a search query - private ArrayList mSearchResults; - private AllAppsGridAdapter mAdapter; + private final ArrayList mSearchResults = new ArrayList<>(); + private BaseAllAppsAdapter mAdapter; private AppInfoComparator mAppNameComparator; - private final int mNumAppsPerRow; + private final int mNumAppsPerRowAllApps; private int mNumAppRowsInAdapter; - private ItemInfoMatcher mItemFilter; + private Predicate mItemFilter; - public AlphabeticalAppsList(Context context, AllAppsStore appsStore, + public AlphabeticalAppsList(Context context, @Nullable AllAppsStore appsStore, WorkAdapterProvider adapterProvider) { mAllAppsStore = appsStore; - mLauncher = BaseDraggingActivity.fromContext(context); + mActivityContext = ActivityContext.lookupContext(context); mAppNameComparator = new AppInfoComparator(context); mWorkAdapterProvider = adapterProvider; - mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns; - mAllAppsStore.addUpdateListener(this); + mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().inv.numAllAppsColumns; + if (mAllAppsStore != null) { + mAllAppsStore.addUpdateListener(this); + } } - public void updateItemFilter(ItemInfoMatcher itemFilter) { + public void updateItemFilter(Predicate itemFilter) { this.mItemFilter = itemFilter; onAppsUpdated(); } @@ -103,17 +111,10 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { /** * Sets the adapter to notify when this dataset changes. */ - public void setAdapter(AllAppsGridAdapter adapter) { + public void setAdapter(BaseAllAppsAdapter adapter) { mAdapter = adapter; } - /** - * Returns all the apps. - */ - public List getApps() { - return mApps; - } - /** * Returns fast scroller sections of all the current filtered applications. */ @@ -165,50 +166,32 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { } /** - * Returns whether there are is a filter set. + * Returns whether there are search results which will hide the A-Z list. */ - public boolean hasFilter() { - return (mSearchResults != null); + public boolean hasSearchResults() { + return !mSearchResults.isEmpty(); } /** * Returns whether there are no filtered results. */ public boolean hasNoFilteredResults() { - return (mSearchResults != null) && mAccessibilityResultsCount == 0; + return hasSearchResults() && mAccessibilityResultsCount == 0; } /** * Sets results list for search */ public boolean setSearchResults(ArrayList results) { - if (!Objects.equals(results, mSearchResults)) { - mSearchResults = results; - updateAdapterItems(); - return true; + if (Objects.equals(results, mSearchResults)) { + return false; } - return false; - } - - public boolean appendSearchResults(ArrayList results) { - if (mSearchResults != null && results != null && results.size() > 0) { - updateSearchAdapterItems(results, mSearchResults.size()); - refreshRecyclerView(); - return true; - } - return false; - } - - void updateSearchAdapterItems(ArrayList list, int offset) { - for (int i = 0; i < list.size(); i++) { - AdapterItem adapterItem = list.get(i); - adapterItem.position = offset + i; - mAdapterItems.add(adapterItem); - - if (adapterItem.isCountedForAccessibility()) { - mAccessibilityResultsCount++; - } + mSearchResults.clear(); + if (results != null) { + mSearchResults.addAll(results); } + updateAdapterItems(); + return true; } /** @@ -216,47 +199,37 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { */ @Override public void onAppsUpdated() { + if (mAllAppsStore == null) { + return; + } // Sort the list of apps mApps.clear(); - for (AppInfo app : mAllAppsStore.getApps()) { - if (mItemFilter == null || mItemFilter.matches(app, null) || hasFilter()) { - mApps.add(app); - } + Stream appSteam = Stream.of(mAllAppsStore.getApps()); + if (!hasSearchResults() && mItemFilter != null) { + appSteam = appSteam.filter(mItemFilter); } - - Collections.sort(mApps, mAppNameComparator); + appSteam = appSteam.sorted(mAppNameComparator); // As a special case for some languages (currently only Simplified Chinese), we may need to // coalesce sections - Locale curLocale = mLauncher.getResources().getConfiguration().locale; + Locale curLocale = mActivityContext.getResources().getConfiguration().locale; boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); if (localeRequiresSectionSorting) { // Compute the section headers. We use a TreeMap with the section name comparator to // ensure that the sections are ordered when we iterate over it later - TreeMap> sectionMap = new TreeMap<>(new LabelComparator()); - for (AppInfo info : mApps) { - // Add the section to the cache - String sectionName = info.sectionName; - - // Add it to the mapping - ArrayList sectionApps = sectionMap.get(sectionName); - if (sectionApps == null) { - sectionApps = new ArrayList<>(); - sectionMap.put(sectionName, sectionApps); - } - sectionApps.add(info); - } - - // Add each of the section apps to the list in order - mApps.clear(); - for (Map.Entry> entry : sectionMap.entrySet()) { - mApps.addAll(entry.getValue()); - } + appSteam = appSteam.collect(Collectors.groupingBy( + info -> info.sectionName, + () -> new TreeMap<>(new LabelComparator()), + Collectors.toCollection(ArrayList::new))) + .values() + .stream() + .flatMap(ArrayList::stream); } + appSteam.forEachOrdered(mApps::add); // Recompose the set of adapter items from the current set of apps - if (mSearchResults == null) { + if (mSearchResults.isEmpty()) { updateAdapterItems(); } } @@ -266,71 +239,50 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { * mCachedSectionNames to have been calculated for the set of all apps in mApps. */ public void updateAdapterItems() { - refillAdapterItems(); - refreshRecyclerView(); - } - - private void refreshRecyclerView() { - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } - } - - private void refillAdapterItems() { - String lastSectionName = null; - FastScrollSectionInfo lastFastScrollerSectionInfo = null; - int position = 0; - int appIndex = 0; - + List oldItems = new ArrayList<>(mAdapterItems); // Prepare to update the list of sections, filtered apps, etc. - mAccessibilityResultsCount = 0; mFastScrollerSections.clear(); mAdapterItems.clear(); + mAccessibilityResultsCount = 0; // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the // ordered set of sections - - if (!hasFilter()) { - mAccessibilityResultsCount = mApps.size(); + if (hasSearchResults()) { + mAdapterItems.addAll(mSearchResults); + if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { + // Append the search market item + if (hasNoFilteredResults()) { + mAdapterItems.add(new AdapterItem(VIEW_TYPE_EMPTY_SEARCH)); + } else { + mAdapterItems.add(new AdapterItem(VIEW_TYPE_ALL_APPS_DIVIDER)); + } + mAdapterItems.add(new AdapterItem(VIEW_TYPE_SEARCH_MARKET)); + } + } else { + int position = 0; if (mWorkAdapterProvider != null) { position += mWorkAdapterProvider.addWorkItems(mAdapterItems); if (!mWorkAdapterProvider.shouldShowWorkApps()) { return; } } + String lastSectionName = null; for (AppInfo info : mApps) { - String sectionName = info.sectionName; + mAdapterItems.add(AdapterItem.asApp(info)); + String sectionName = info.sectionName; // Create a new section if the section names do not match if (!sectionName.equals(lastSectionName)) { lastSectionName = sectionName; - lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); - mFastScrollerSections.add(lastFastScrollerSectionInfo); + mFastScrollerSections.add(new FastScrollSectionInfo(sectionName, position)); } - - // Create an app item - AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, - appIndex++); - if (lastFastScrollerSectionInfo.fastScrollToItem == null) { - lastFastScrollerSectionInfo.fastScrollToItem = appItem; - } - - mAdapterItems.add(appItem); - } - } else { - updateSearchAdapterItems(mSearchResults, 0); - if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { - // Append the search market item - if (hasNoFilteredResults()) { - mAdapterItems.add(AdapterItem.asEmptySearch(position++)); - } else { - mAdapterItems.add(AdapterItem.asAllAppsDivider(position++)); - } - mAdapterItems.add(AdapterItem.asMarketSearch(position++)); - + position++; } } - if (mNumAppsPerRow != 0) { + mAccessibilityResultsCount = (int) mAdapterItems.stream() + .filter(AdapterItem::isCountedForAccessibility).count(); + + if (mNumAppsPerRowAllApps != 0) { // Update the number of rows in the adapter after we do all the merging (otherwise, we // would have to shift the values again) int numAppsInSection = 0; @@ -338,10 +290,10 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { int rowIndex = -1; for (AdapterItem item : mAdapterItems) { item.rowIndex = 0; - if (AllAppsGridAdapter.isDividerViewType(item.viewType)) { + if (BaseAllAppsAdapter.isDividerViewType(item.viewType)) { numAppsInSection = 0; - } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) { - if (numAppsInSection % mNumAppsPerRow == 0) { + } else if (BaseAllAppsAdapter.isIconViewType(item.viewType)) { + if (numAppsInSection % mNumAppsPerRowAllApps == 0) { numAppsInRow = 0; rowIndex++; } @@ -352,36 +304,43 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { } } mNumAppRowsInAdapter = rowIndex + 1; + } - // Pre-calculate all the fast scroller fractions - switch (mFastScrollDistributionMode) { - case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: - float rowFraction = 1f / mNumAppRowsInAdapter; - for (FastScrollSectionInfo info : mFastScrollerSections) { - AdapterItem item = info.fastScrollToItem; - if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { - info.touchFraction = 0f; - continue; - } - - float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); - info.touchFraction = item.rowIndex * rowFraction + subRowFraction; - } - break; - case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: - float perSectionTouchFraction = 1f / mFastScrollerSections.size(); - float cumulativeTouchFraction = 0f; - for (FastScrollSectionInfo info : mFastScrollerSections) { - AdapterItem item = info.fastScrollToItem; - if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { - info.touchFraction = 0f; - continue; - } - info.touchFraction = cumulativeTouchFraction; - cumulativeTouchFraction += perSectionTouchFraction; - } - break; - } + if (mAdapter != null) { + DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false) + .dispatchUpdatesTo(mAdapter); } } + + private static class MyDiffCallback extends DiffUtil.Callback { + + private final List mOldList; + private final List mNewList; + + MyDiffCallback(List oldList, List newList) { + mOldList = oldList; + mNewList = newList; + } + + @Override + public int getOldListSize() { + return mOldList.size(); + } + + @Override + public int getNewListSize() { + return mNewList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition)); + } + } + } diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java index 823f98efea..311a40ef64 100644 --- a/src/com/android/launcher3/allapps/AppInfoComparator.java +++ b/src/com/android/launcher3/allapps/AppInfoComparator.java @@ -43,7 +43,9 @@ public class AppInfoComparator implements Comparator { @Override public int compare(AppInfo a, AppInfo b) { // Order by the title in the current locale - int result = mLabelComparator.compare(a.title.toString(), b.title.toString()); + int result = mLabelComparator.compare( + a.title == null ? "" : a.title.toString(), + b.title == null ? "" : b.title.toString()); if (result != 0) { return result; } diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java new file mode 100644 index 0000000000..fcba246c95 --- /dev/null +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import static com.android.launcher3.touch.ItemLongClickListener.INSTANCE_ALL_APPS; + +import android.content.Context; +import android.content.res.Resources; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.R; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.views.ActivityContext; + +import java.util.Arrays; + +/** + * Adapter for all the apps. + * + * @param Type of context inflating all apps. + */ +public abstract class BaseAllAppsAdapter extends + RecyclerView.Adapter { + + public static final String TAG = "BaseAllAppsAdapter"; + + // A normal icon + public static final int VIEW_TYPE_ICON = 1 << 1; + // The message shown when there are no filtered results + public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2; + // The message to continue to a market search when there are no filtered results + public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3; + + // We use various dividers for various purposes. They share enough attributes to reuse layouts, + // but differ in enough attributes to require different view types + + // A divider that separates the apps list and the search market button + public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4; + + // Common view type masks + public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; + public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; + + + protected final BaseAdapterProvider[] mAdapterProviders; + + /** + * ViewHolder for each icon. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View v) { + super(v); + } + } + + /** Sets the number of apps to be displayed in one row of the all apps screen. */ + public abstract void setAppsPerRow(int appsPerRow); + + /** + * Info about a particular adapter item (can be either section or app) + */ + public static class AdapterItem { + /** Common properties */ + // The type of this item + public final int viewType; + + // The row that this item shows up on + public int rowIndex; + // The index of this app in the row + public int rowAppIndex; + // The associated ItemInfoWithIcon for the item + public AppInfo itemInfo = null; + + public AdapterItem(int viewType) { + this.viewType = viewType; + } + + /** + * Factory method for AppIcon AdapterItem + */ + public static AdapterItem asApp(AppInfo appInfo) { + AdapterItem item = new AdapterItem(VIEW_TYPE_ICON); + item.itemInfo = appInfo; + return item; + } + + protected boolean isCountedForAccessibility() { + return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET; + } + + /** + * Returns true if the items represent the same object + */ + public boolean isSameAs(AdapterItem other) { + return (other.viewType == viewType) && (other.getClass() == getClass()); + } + + /** + * This is called only if {@link #isSameAs} returns true to check if the contents are same + * as well. Returning true will prevent redrawing of thee item. + */ + public boolean isContentSame(AdapterItem other) { + return itemInfo == null && other.itemInfo == null; + } + } + + protected final T mActivityContext; + protected final AlphabeticalAppsList mApps; + // The text to show when there are no search results and no market search handler. + protected String mEmptySearchMessage; + protected int mAppsPerRow; + + protected final LayoutInflater mLayoutInflater; + protected final OnClickListener mOnIconClickListener; + protected OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS; + protected OnFocusChangeListener mIconFocusListener; + // The click listener to send off to the market app, updated each time the search query changes. + private OnClickListener mMarketSearchClickListener; + private final int mExtraHeight; + + public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater, + AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) { + Resources res = activityContext.getResources(); + mActivityContext = activityContext; + mApps = apps; + mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); + mLayoutInflater = inflater; + + mOnIconClickListener = mActivityContext.getItemOnClickListener(); + + mAdapterProviders = adapterProviders; + mExtraHeight = res.getDimensionPixelSize(R.dimen.all_apps_height_extra); + } + + /** + * Sets the long click listener for icons + */ + public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) { + mOnIconLongClickListener = listener; + } + + /** Checks if the passed viewType represents all apps divider. */ + public static boolean isDividerViewType(int viewType) { + return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); + } + + /** Checks if the passed viewType represents all apps icon. */ + public static boolean isIconViewType(int viewType) { + return isViewType(viewType, VIEW_TYPE_MASK_ICON); + } + + public void setIconFocusListener(OnFocusChangeListener focusListener) { + mIconFocusListener = focusListener; + } + + /** + * Sets the last search query that was made, used to show when there are no results and to also + * seed the intent for searching the market. + */ + public void setLastSearchQuery(String query, OnClickListener marketSearchClickListener) { + Resources res = mActivityContext.getResources(); + mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); + mMarketSearchClickListener = marketSearchClickListener; + } + + /** + * Returns the layout manager. + */ + public abstract RecyclerView.LayoutManager getLayoutManager(); + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ICON: + int layout = !FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() ? R.layout.all_apps_icon + : R.layout.all_apps_icon_twoline; + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + layout, parent, false); + icon.setLongPressTimeoutFactor(1f); + icon.setOnFocusChangeListener(mIconFocusListener); + icon.setOnClickListener(mOnIconClickListener); + icon.setOnLongClickListener(mOnIconLongClickListener); + // Ensure the all apps icon height matches the workspace icons in portrait mode. + icon.getLayoutParams().height = + mActivityContext.getDeviceProfile().allAppsCellHeightPx; + if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) { + icon.getLayoutParams().height += mExtraHeight; + } + return new ViewHolder(icon); + case VIEW_TYPE_EMPTY_SEARCH: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, + parent, false)); + case VIEW_TYPE_SEARCH_MARKET: + View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, + parent, false); + searchMarketView.setOnClickListener(mMarketSearchClickListener); + return new ViewHolder(searchMarketView); + case VIEW_TYPE_ALL_APPS_DIVIDER: + return new ViewHolder(mLayoutInflater.inflate( + R.layout.all_apps_divider, parent, false)); + default: + BaseAdapterProvider adapterProvider = getAdapterProvider(viewType); + if (adapterProvider != null) { + return adapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType); + } + throw new RuntimeException("Unexpected view type" + viewType); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case VIEW_TYPE_ICON: + AdapterItem adapterItem = mApps.getAdapterItems().get(position); + BubbleTextView icon = (BubbleTextView) holder.itemView; + icon.reset(); + icon.applyFromApplicationInfo(adapterItem.itemInfo); + break; + case VIEW_TYPE_EMPTY_SEARCH: + TextView emptyViewText = (TextView) holder.itemView; + emptyViewText.setText(mEmptySearchMessage); + emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : + Gravity.START | Gravity.CENTER_VERTICAL); + break; + case VIEW_TYPE_SEARCH_MARKET: + TextView searchView = (TextView) holder.itemView; + if (mMarketSearchClickListener != null) { + searchView.setVisibility(View.VISIBLE); + } else { + searchView.setVisibility(View.GONE); + } + break; + case VIEW_TYPE_ALL_APPS_DIVIDER: + // nothing to do + break; + default: + BaseAdapterProvider adapterProvider = getAdapterProvider(holder.getItemViewType()); + if (adapterProvider != null) { + adapterProvider.onBindView(holder, position); + } + } + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + super.onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(ViewHolder holder) { + // Always recycle and we will reset the view when it is bound + return true; + } + + @Override + public int getItemCount() { + return mApps.getAdapterItems().size(); + } + + @Override + public int getItemViewType(int position) { + AdapterItem item = mApps.getAdapterItems().get(position); + return item.viewType; + } + + protected static boolean isViewType(int viewType, int viewTypeMask) { + return (viewType & viewTypeMask) != 0; + } + + @Nullable + protected BaseAdapterProvider getAdapterProvider(int viewType) { + return Arrays.stream(mAdapterProviders).filter( + adapterProvider -> adapterProvider.isViewSupported(viewType)).findFirst().orElse( + null); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java similarity index 51% rename from src/com/android/launcher3/allapps/AllAppsContainerView.java rename to src/com/android/launcher3/allapps/BaseAllAppsContainerView.java index 3ba6ea4de9..ecadec673a 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java @@ -17,6 +17,7 @@ package com.android.launcher3.allapps; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; +import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -31,27 +32,23 @@ import android.os.Bundle; import android.os.Parcelable; import android.os.Process; import android.os.UserManager; -import android.text.SpannableStringBuilder; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.WindowInsets; +import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.ColorUtils; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DeviceProfile.DeviceProfileListenable; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget.DragObject; @@ -60,24 +57,34 @@ import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.search.SearchAdapterProvider; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.keyboard.FocusedItemDecorator; -import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.StringCache; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.ScrimView; import com.android.launcher3.views.SpringRelativeLayout; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + /** - * The all apps view container. + * Base all apps view container. + * + * @param Type of context inflating all apps. */ -public class AllAppsContainerView extends SpringRelativeLayout implements DragSource, - Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener, +public abstract class BaseAllAppsContainerView extends SpringRelativeLayout implements DragSource, Insettable, + OnDeviceProfileChangeListener, OnActivePageChangedListener, ScrimView.ScrimDrawingController { - private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; + protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; public static final float PULL_MULTIPLIER = .02f; public static final float FLING_VELOCITY_MULTIPLIER = 1200f; @@ -85,10 +92,12 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Rect mInsets = new Rect(); - protected final BaseDraggingActivity mLauncher; - protected final AdapterHolder[] mAH; - protected final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser( + /** Context of an activity or window that is inflating this container. */ + protected final T mActivityContext; + protected final List mAH; + protected final Predicate mPersonalMatcher = ItemInfoMatcher.ofUser( Process.myUserHandle()); + private final SearchAdapterProvider mMainAdapterProvider; private final AllAppsStore mAllAppsStore = new AllAppsStore(); private final RecyclerView.OnScrollListener mScrollListener = @@ -100,67 +109,64 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo }; private final WorkProfileManager mWorkManager; - private final Paint mNavBarScrimPaint; private int mNavBarScrimHeight = 0; - protected SearchUiManager mSearchUiManager; - private View mSearchContainer; private AllAppsPagedView mViewPager; + private SearchRecyclerView mSearchRecyclerView; protected FloatingHeaderView mHeader; - - - private SpannableStringBuilder mSearchQueryBuilder = null; + private View mBottomSheetBackground; + private View mBottomSheetHandleArea; protected boolean mUsingTabs; - private boolean mIsSearching; private boolean mHasWorkApps; protected RecyclerViewFastScroller mTouchHandler; protected final Point mFastScrollerOffset = new Point(); - private SearchAdapterProvider mSearchAdapterProvider; private final int mScrimColor; private final int mHeaderProtectionColor; - private final float mHeaderThreshold; + protected final float mHeaderThreshold; + private int mHeaderBottomAdjustment; private ScrimView mScrimView; private int mHeaderColor; private int mTabsProtectionAlpha; - public AllAppsContainerView(Context context) { - this(context, null); - } - - public AllAppsContainerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + protected BaseAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - - mLauncher = BaseDraggingActivity.fromContext(context); + mActivityContext = ActivityContext.lookupContext(context); + mMainAdapterProvider = createMainAdapterProvider(); mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); mHeaderThreshold = getResources().getDimensionPixelSize( R.dimen.dynamic_grid_cell_border_spacing); + mHeaderBottomAdjustment = getResources().getDimensionPixelSize( + R.dimen.all_apps_header_bottom_adjustment); mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); - mLauncher.addOnDeviceProfileChangeListener(this); - - mSearchAdapterProvider = mLauncher.createSearchAdapterProvider(this); - - mAH = new AdapterHolder[2]; - - mWorkManager = new WorkProfileManager(mLauncher.getSystemService(UserManager.class), this, - Utilities.getPrefs(mLauncher)); - mAH[AdapterHolder.MAIN] = new AdapterHolder(false /* isWork */); - mAH[AdapterHolder.WORK] = new AdapterHolder(true /* isWork */); + mWorkManager = new WorkProfileManager( + mActivityContext.getSystemService(UserManager.class), + this, + Utilities.getPrefs(mActivityContext), mActivityContext.getDeviceProfile()); + mAH = Arrays.asList(null, null, null); + mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN)); + mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); + mAH.set(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); mNavBarScrimPaint = new Paint(); mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); mAllAppsStore.addUpdateListener(this::onAppsUpdated); + mActivityContext.addOnDeviceProfileChangeListener(this); + } + + /** Creates the adapter provider for the main section. */ + protected abstract SearchAdapterProvider createMainAdapterProvider(); + + /** The adapter provider for the main section. */ + public final SearchAdapterProvider getMainAdapterProvider() { + return mMainAdapterProvider; } @Override @@ -178,7 +184,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); if (state != null) { int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); - if (currentPage != 0 && mViewPager != null) { + if (currentPage == AdapterHolder.WORK && mViewPager != null) { mViewPager.setCurrentPage(currentPage); rebindAdapters(); } else { @@ -201,7 +207,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo */ public void setOnIconLongClickListener(OnLongClickListener listener) { for (AdapterHolder holder : mAH) { - holder.adapter.setOnIconLongClickListener(listener); + holder.mAdapter.setOnIconLongClickListener(listener); } } @@ -216,28 +222,26 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo @Override public void onDeviceProfileChanged(DeviceProfile dp) { for (AdapterHolder holder : mAH) { - holder.adapter.setAppsPerRow(dp.numShownAllAppsColumns); - if (holder.recyclerView != null) { + holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); + if (holder.mRecyclerView != null) { // Remove all views and clear the pool, while keeping the data same. After this // call, all the viewHolders will be recreated. - holder.recyclerView.swapAdapter(holder.recyclerView.getAdapter(), true); - holder.recyclerView.getRecycledViewPool().clear(); + holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); + holder.mRecyclerView.getRecycledViewPool().clear(); } } + updateBackground(dp); + } + + protected void updateBackground(DeviceProfile deviceProfile) { + mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? View.VISIBLE : View.GONE); } private void onAppsUpdated() { - boolean hasWorkApps = false; - for (AppInfo app : mAllAppsStore.getApps()) { - if (mWorkManager.getMatcher().matches(app, null)) { - hasWorkApps = true; - break; - } - } - mHasWorkApps = hasWorkApps; - if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) { + mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher()); + if (!isSearching()) { rebindAdapters(); - if (hasWorkApps) { + if (mHasWorkApps) { mWorkManager.reset(); } } @@ -247,28 +251,32 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo * Returns whether the view itself will handle the touch event or not. */ public boolean shouldContainerScroll(MotionEvent ev) { - // IF the MotionEvent is inside the search box, and the container keeps on receiving - // touch input, container should move down. - if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) { + BaseDragLayer dragLayer = mActivityContext.getDragLayer(); + // Scroll if not within the container view (e.g. over large-screen scrim). + if (!dragLayer.isEventOverView(this, ev)) { + return true; + } + if (dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { return true; } AllAppsRecyclerView rv = getActiveRecyclerView(); if (rv == null) { return true; } - if (rv.getScrollbar().getThumbOffsetY() >= 0 && - mLauncher.getDragLayer().isEventOverView(rv.getScrollbar(), ev)) { + if (rv.getScrollbar() != null + && rv.getScrollbar().getThumbOffsetY() >= 0 + && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { return false; } - return rv.shouldContainerScroll(ev, mLauncher.getDragLayer()); + return rv.shouldContainerScroll(ev, dragLayer); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { AllAppsRecyclerView rv = getActiveRecyclerView(); - if (rv != null && - rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { + if (rv != null && rv.getScrollbar() != null + && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { mTouchHandler = rv.getScrollbar(); } else { mTouchHandler = null; @@ -284,8 +292,8 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { AllAppsRecyclerView rv = getActiveRecyclerView(); - if (rv != null && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), - mFastScrollerOffset)) { + if (rv != null && rv.getScrollbar() != null + && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { mTouchHandler = rv.getScrollbar(); } else { mTouchHandler = null; @@ -296,29 +304,71 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); return true; } + if (isSearching()) { + // if in search state, consume touch event. + return true; + } return false; } + /** Description of the container view based on its current state. */ public String getDescription() { - @StringRes int descriptionRes; + StringCache cache = mActivityContext.getStringCache(); if (mUsingTabs) { - descriptionRes = - mViewPager.getNextPage() == 0 - ? R.string.all_apps_button_personal_label - : R.string.all_apps_button_work_label; - } else if (mIsSearching) { - descriptionRes = R.string.all_apps_search_results; - } else { - descriptionRes = R.string.all_apps_button_label; + if (cache != null) { + return isPersonalTab() + ? cache.allAppsPersonalTabAccessibility + : cache.allAppsWorkTabAccessibility; + } else { + return isPersonalTab() + ? getContext().getString(R.string.all_apps_button_personal_label) + : getContext().getString(R.string.all_apps_button_work_label); + } } - return getContext().getString(descriptionRes); + return getContext().getString(R.string.all_apps_button_label); } + /** The current active recycler view (A-Z list from one of the profiles, or search results). */ public AllAppsRecyclerView getActiveRecyclerView() { - if (!mUsingTabs || mViewPager.getNextPage() == 0) { - return mAH[AdapterHolder.MAIN].recyclerView; + if (isSearching()) { + return getSearchRecyclerView(); + } + return getActiveAppsRecyclerView(); + } + + /** The current apps recycler view in the container. */ + private AllAppsRecyclerView getActiveAppsRecyclerView() { + if (!mUsingTabs || isPersonalTab()) { + return mAH.get(AdapterHolder.MAIN).mRecyclerView; } else { - return mAH[AdapterHolder.WORK].recyclerView; + return mAH.get(AdapterHolder.WORK).mRecyclerView; + } + } + + /** + * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently + * hidden while searching. + **/ + private View getAppsRecyclerViewContainer() { + return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); + } + + /** The RV for search results, which is hidden while A-Z apps are visible. */ + public SearchRecyclerView getSearchRecyclerView() { + return mSearchRecyclerView; + } + + protected boolean isPersonalTab() { + return mViewPager == null || mViewPager.getNextPage() == 0; + } + + /** + * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does + * nothing. + */ + public void switchToTab(int tab) { + if (mUsingTabs) { + mViewPager.setCurrentPage(tab); } } @@ -330,16 +380,15 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo * Resets the state of AllApps. */ public void reset(boolean animate) { - for (int i = 0; i < mAH.length; i++) { - if (mAH[i].recyclerView != null) { - mAH[i].recyclerView.scrollToTop(); + for (int i = 0; i < mAH.size(); i++) { + if (mAH.get(i).mRecyclerView != null) { + mAH.get(i).mRecyclerView.scrollToTop(); } } if (isHeaderVisible()) { mHeader.reset(animate); } - // Reset the search bar and base recycler view after transitioning home - mSearchUiManager.resetSearch(); + // Reset the base recycler view after transitioning home. updateHeaderScroll(0); } @@ -356,39 +405,28 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo }); mHeader = findViewById(R.id.all_apps_header); + mSearchRecyclerView = findViewById(R.id.search_results_list_view); + mAH.get(AdapterHolder.SEARCH).setup(mSearchRecyclerView, + /* Filter out A-Z apps */ itemInfo -> false); rebindAdapters(true /* force */); - mSearchContainer = findViewById(R.id.search_container_all_apps); - mSearchUiManager = (SearchUiManager) mSearchContainer; - mSearchUiManager.initializeSearch(this); - } + mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); + updateBackground(mActivityContext.getDeviceProfile()); - public SearchUiManager getSearchUiManager() { - return mSearchUiManager; + mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); } @Override - public boolean dispatchKeyEvent(KeyEvent event) { - mSearchUiManager.preDispatchKeyEvent(event); - return super.dispatchKeyEvent(event); - } - - @Override - public void onDropCompleted(View target, DragObject d, boolean success) { - } + public void onDropCompleted(View target, DragObject d, boolean success) {} @Override public void setInsets(Rect insets) { mInsets.set(insets); - DeviceProfile grid = mLauncher.getDeviceProfile(); + DeviceProfile grid = mActivityContext.getDeviceProfile(); - for (int i = 0; i < mAH.length; i++) { - mAH[i].padding.bottom = insets.bottom; - mAH[i].padding.left = mAH[i].padding.right = grid.allAppsLeftRightPadding; - mAH[i].applyPadding(); - } + applyAdapterSideAndBottomPaddings(grid); - ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); + MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); mlp.leftMargin = insets.left; mlp.rightMargin = insets.right; setLayoutParams(mlp); @@ -396,19 +434,24 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo if (grid.isVerticalBarLayout()) { setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); } else { - setPadding(0, 0, 0, 0); + setPadding(grid.allAppsLeftRightMargin, grid.allAppsTopPadding, + grid.allAppsLeftRightMargin, 0); } InsettableFrameLayout.dispatchInsets(this, insets); } + /** + * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. + */ + protected int getNavBarScrimHeight(WindowInsets insets) { + return 0; + } + @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { - if (Utilities.ATLEAST_Q) { - mNavBarScrimHeight = insets.getTappableElementInsets().bottom; - } else { - mNavBarScrimHeight = insets.getStableInsetBottom(); - } + mNavBarScrimHeight = getNavBarScrimHeight(insets); + applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); return super.dispatchApplyWindowInsets(insets); } @@ -422,60 +465,118 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo } } - private void rebindAdapters() { + protected void rebindAdapters() { rebindAdapters(false /* force */); } protected void rebindAdapters(boolean force) { - boolean showTabs = mHasWorkApps && !mIsSearching; + updateSearchResultsVisibility(); + + boolean showTabs = shouldShowTabs(); if (showTabs == mUsingTabs && !force) { return; } - replaceRVContainer(showTabs); + + if (isSearching()) { + mUsingTabs = showTabs; + mWorkManager.detachWorkModeSwitch(); + return; + } + + // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND + // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen + // after this call. + replaceAppsRVContainer(showTabs); mUsingTabs = showTabs; - mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.MAIN].recyclerView); - mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView); + mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); + mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); if (mUsingTabs) { - mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher); - mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); - mAH[AdapterHolder.WORK].recyclerView.setId(R.id.apps_list_view_work); + mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher); + mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); + mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work); mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); findViewById(R.id.tab_personal) .setOnClickListener((View view) -> { if (mViewPager.snapToPage(AdapterHolder.MAIN)) { - mLauncher.getStatsLogManager().logger() + mActivityContext.getStatsLogManager().logger() .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); } + hideKeyboardAsync(ActivityContext.lookupContext(getContext()), + getApplicationWindowToken()); }); findViewById(R.id.tab_work) .setOnClickListener((View view) -> { if (mViewPager.snapToPage(AdapterHolder.WORK)) { - mLauncher.getStatsLogManager().logger() + mActivityContext.getStatsLogManager().logger() .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); } + hideKeyboardAsync(ActivityContext.lookupContext(getContext()), + getApplicationWindowToken()); }); + setDeviceManagementResources(); onActivePageChanged(mViewPager.getNextPage()); } else { - mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null); - mAH[AdapterHolder.WORK].recyclerView = null; + mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null); + mAH.get(AdapterHolder.WORK).mRecyclerView = null; } setupHeader(); - mAllAppsStore.registerIconContainer(mAH[AdapterHolder.MAIN].recyclerView); - mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView); + mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); + mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); } + private void updateSearchResultsVisibility() { + if (isSearching()) { + getSearchRecyclerView().setVisibility(VISIBLE); + getAppsRecyclerViewContainer().setVisibility(GONE); + } else { + getSearchRecyclerView().setVisibility(GONE); + getAppsRecyclerViewContainer().setVisibility(VISIBLE); + } + if (mHeader.isSetUp()) { + mHeader.setActiveRV(getCurrentPage()); + } + } - private void replaceRVContainer(boolean showTabs) { - for (AdapterHolder adapterHolder : mAH) { - if (adapterHolder.recyclerView != null) { - adapterHolder.recyclerView.setLayoutManager(null); - adapterHolder.recyclerView.setAdapter(null); + private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { + int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); + mAH.forEach(adapterHolder -> { + adapterHolder.mPadding.bottom = bottomPadding; + adapterHolder.mPadding.left = + adapterHolder.mPadding.right = grid.allAppsLeftRightPadding; + adapterHolder.applyPadding(); + }); + } + + private void setDeviceManagementResources() { + if (mActivityContext.getStringCache() != null) { + Button personalTab = findViewById(R.id.tab_personal); + personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); + + Button workTab = findViewById(R.id.tab_work); + workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); + } + } + + protected boolean shouldShowTabs() { + return mHasWorkApps; + } + + protected boolean isSearching() { + return false; + } + + protected View replaceAppsRVContainer(boolean showTabs) { + for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { + AdapterHolder adapterHolder = mAH.get(i); + if (adapterHolder.mRecyclerView != null) { + adapterHolder.mRecyclerView.setLayoutManager(null); + adapterHolder.mRecyclerView.setAdapter(null); } } - View oldView = getRecyclerViewContainer(); + View oldView = getAppsRecyclerViewContainer(); int index = indexOfChild(oldView); removeView(oldView); int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; @@ -486,23 +587,20 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo mViewPager.initParentViews(this); mViewPager.getPageIndicator().setOnActivePageChangedListener(this); if (mWorkManager.attachWorkModeSwitch()) { - mWorkManager.getWorkModeSwitch().post(() -> mAH[AdapterHolder.WORK].applyPadding()); + mWorkManager.getWorkModeSwitch().post( + () -> mAH.get(AdapterHolder.WORK).applyPadding()); } } else { mWorkManager.detachWorkModeSwitch(); mViewPager = null; } - } - - public View getRecyclerViewContainer() { - return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); + return newView; } @Override public void onActivePageChanged(int currentActivePage) { - mHeader.setMainActive(currentActivePage == AdapterHolder.MAIN); - if (mAH[currentActivePage].recyclerView != null) { - mAH[currentActivePage].recyclerView.bindFastScrollbar(); + if (mAH.get(currentActivePage).mRecyclerView != null) { + mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(); } reset(true /* animate */); @@ -524,96 +622,55 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo return isDescendantViewVisible(R.id.tab_personal); } - // Used by tests only + @VisibleForTesting public boolean isWorkTabVisible() { return isDescendantViewVisible(R.id.tab_work); } - public AlphabeticalAppsList getApps() { - return mAH[AdapterHolder.MAIN].appsList; + public AlphabeticalAppsList getSearchResultList() { + return mAH.get(AdapterHolder.SEARCH).mAppsList; } public FloatingHeaderView getFloatingHeaderView() { return mHeader; } - public View getSearchView() { - return mSearchContainer; - } - + @VisibleForTesting public View getContentView() { - return mViewPager == null ? getActiveRecyclerView() : mViewPager; + return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); } + /** The current page visible in all apps. */ public int getCurrentPage() { - return mViewPager != null ? mViewPager.getCurrentPage() : AdapterHolder.MAIN; - } - - /** - * Handles selection on focused view and returns success - */ - public boolean launchHighlightedItem() { - if (mSearchAdapterProvider == null) return false; - return mSearchAdapterProvider.launchHighlightedItem(); - } - - public SearchAdapterProvider getSearchAdapterProvider() { - return mSearchAdapterProvider; + return isSearching() + ? AdapterHolder.SEARCH + : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); } + /** The scroll bar for the active apps recycler view. */ public RecyclerViewFastScroller getScrollBar() { - AllAppsRecyclerView rv = getActiveRecyclerView(); + AllAppsRecyclerView rv = getActiveAppsRecyclerView(); return rv == null ? null : rv.getScrollbar(); } - public void setupHeader() { + void setupHeader() { mHeader.setVisibility(View.VISIBLE); - mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null); + boolean tabsHidden = !mUsingTabs; + mHeader.setup( + mAH.get(AdapterHolder.MAIN).mRecyclerView, + mAH.get(AdapterHolder.WORK).mRecyclerView, + (SearchRecyclerView) mAH.get(AdapterHolder.SEARCH).mRecyclerView, + getCurrentPage(), + tabsHidden); int padding = mHeader.getMaxTranslation(); - for (int i = 0; i < mAH.length; i++) { - mAH[i].padding.top = padding; - mAH[i].applyPadding(); - if (mAH[i].recyclerView != null) { - mAH[i].recyclerView.scrollToTop(); + mAH.forEach(adapterHolder -> { + adapterHolder.mPadding.top = padding; + adapterHolder.applyPadding(); + if (adapterHolder.mRecyclerView != null) { + adapterHolder.mRecyclerView.scrollToTop(); } - } - } - - public void setLastSearchQuery(String query) { - for (int i = 0; i < mAH.length; i++) { - mAH[i].adapter.setLastSearchQuery(query); - } - mIsSearching = true; - rebindAdapters(); - mHeader.setCollapsed(true); - } - - public void onClearSearchResult() { - mIsSearching = false; - mHeader.setCollapsed(false); - rebindAdapters(); - mHeader.reset(false); - } - - public void onSearchResultsChanged() { - for (int i = 0; i < mAH.length; i++) { - if (mAH[i].recyclerView != null) { - mAH[i].recyclerView.onSearchResultsChanged(); - } - } - } - - public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) { - for (int i = 0; i < mAH.length; i++) { - mAH[i].applyVerticalFadingEdgeEnabled(enabled); - } - } - - public void addElevationController(RecyclerView.OnScrollListener scrollListener) { - if (!mUsingTabs) { - mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener); - } + }); } public boolean isHeaderVisible() { @@ -621,7 +678,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo } /** - * Adds an update listener to {@param animator} that adds springs to the animation. + * Adds an update listener to animator that adds springs to the animation. */ public void addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity /* release velocity */, @@ -629,7 +686,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { - float distance = (float) ((1 - progress) * getHeight()); // px + float distance = (1 - progress) * getHeight(); // px float settleVelocity = Math.min(0, distance / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) + velocity); @@ -639,6 +696,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo }); } + /** Invoked when the container is pulled. */ public void onPull(float deltaDistance, float displacement) { absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); // Current motion spec is to actually push and not pull @@ -664,11 +722,16 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo @Override public void drawOnScrim(Canvas canvas) { - if (!mHeader.isHeaderProtectionSupported()) return; + if (!mHeader.isHeaderProtectionSupported()) { + return; + } mHeaderPaint.setColor(mHeaderColor); mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); if (mHeaderPaint.getColor() != mScrimColor && mHeaderPaint.getColor() != 0) { - int bottom = (int) (mSearchContainer.getBottom() + getTranslationY()); + int bottom = getHeaderBottom(); + if (!mUsingTabs) { + bottom += getFloatingHeaderView().getPaddingBottom() - mHeaderBottomAdjustment; + } canvas.drawRect(0, 0, canvas.getWidth(), bottom, mHeaderPaint); int tabsHeight = getFloatingHeaderView().getPeripheralProtectionHeight(); if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { @@ -678,101 +741,6 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo } } - public class AdapterHolder { - public static final int MAIN = 0; - public static final int WORK = 1; - - private final boolean mIsWork; - public final AllAppsGridAdapter adapter; - final LinearLayoutManager layoutManager; - final AlphabeticalAppsList appsList; - final Rect padding = new Rect(); - AllAppsRecyclerView recyclerView; - boolean verticalFadingEdge; - - - AdapterHolder(boolean isWork) { - mIsWork = isWork; - appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, - isWork ? mWorkManager.getAdapterProvider() : null); - - BaseAdapterProvider[] adapterProviders = - isWork ? new BaseAdapterProvider[]{mSearchAdapterProvider, - mWorkManager.getAdapterProvider()} - : new BaseAdapterProvider[]{mSearchAdapterProvider}; - - adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList, - adapterProviders); - appsList.setAdapter(adapter); - layoutManager = adapter.getLayoutManager(); - } - - void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) { - appsList.updateItemFilter(matcher); - recyclerView = (AllAppsRecyclerView) rv; - recyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); - recyclerView.setApps(appsList); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(adapter); - recyclerView.setHasFixedSize(true); - // No animations will occur when changes occur to the items in this RecyclerView. - recyclerView.setItemAnimator(null); - recyclerView.addOnScrollListener(mScrollListener); - FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView); - recyclerView.addItemDecoration(focusedItemDecorator); - adapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); - applyVerticalFadingEdgeEnabled(verticalFadingEdge); - applyPadding(); - if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { - recyclerView.addItemDecoration(mSearchAdapterProvider.getDecorator()); - } - } - - void applyPadding() { - if (recyclerView != null) { - int bottomOffset = 0; - if (mIsWork && mWorkManager.getWorkModeSwitch() != null) { - bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); - } - recyclerView.setPadding(padding.left, padding.top, padding.right, - padding.bottom + bottomOffset); - } - } - - public void applyVerticalFadingEdgeEnabled(boolean enabled) { - verticalFadingEdge = enabled; - mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs - && verticalFadingEdge); - } - } - - - protected void updateHeaderScroll(int scrolledOffset) { - - float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); - int viewBG = ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, prog); - int headerColor = ColorUtils.setAlphaComponent(viewBG, - (int) (getSearchView().getAlpha() * 255)); - int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 - : (int) (Utilities.boundToRange( - (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) - * 255); - if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { - mHeaderColor = headerColor; - mTabsProtectionAlpha = tabsAlpha; - invalidateHeader(); - } - if (mSearchUiManager.getEditText() != null) { - boolean bgVisible = mSearchUiManager.getBackgroundVisibility(); - if (scrolledOffset == 0 && !mIsSearching) { - bgVisible = true; - } else if (scrolledOffset > mHeaderThreshold) { - bgVisible = false; - } - mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); - } - } - /** * redraws header protection */ @@ -781,4 +749,102 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo mScrimView.invalidate(); } } + + protected void updateHeaderScroll(int scrolledOffset) { + float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); + int headerColor = getHeaderColor(prog); + int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 + : (int) (Utilities.boundToRange( + (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) + * 255); + if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { + mHeaderColor = headerColor; + mTabsProtectionAlpha = tabsAlpha; + invalidateHeader(); + } + } + + protected int getHeaderColor(float blendRatio) { + return ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio); + } + + protected abstract BaseAllAppsAdapter createAdapter(AlphabeticalAppsList mAppsList, + BaseAdapterProvider[] adapterProviders); + + public int getHeaderBottom() { + return (int) getTranslationY(); + } + + /** + * Returns a view that denotes the visible part of all apps container view. + */ + public View getVisibleContainerView() { + return mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this; + } + + /** Holds a {@link BaseAllAppsAdapter} and related fields. */ + public class AdapterHolder { + public static final int MAIN = 0; + public static final int WORK = 1; + public static final int SEARCH = 2; + + private final int mType; + public final BaseAllAppsAdapter mAdapter; + final RecyclerView.LayoutManager mLayoutManager; + final AlphabeticalAppsList mAppsList; + final Rect mPadding = new Rect(); + AllAppsRecyclerView mRecyclerView; + + AdapterHolder(int type) { + mType = type; + mAppsList = new AlphabeticalAppsList<>(mActivityContext, + isSearch() ? null : mAllAppsStore, + isWork() ? mWorkManager.getAdapterProvider() : null); + + BaseAdapterProvider[] adapterProviders = + isWork() ? new BaseAdapterProvider[]{mMainAdapterProvider, + mWorkManager.getAdapterProvider()} + : new BaseAdapterProvider[]{mMainAdapterProvider}; + + mAdapter = createAdapter(mAppsList, adapterProviders); + mAppsList.setAdapter(mAdapter); + mLayoutManager = mAdapter.getLayoutManager(); + } + + void setup(@NonNull View rv, @Nullable Predicate matcher) { + mAppsList.updateItemFilter(matcher); + mRecyclerView = (AllAppsRecyclerView) rv; + mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); + mRecyclerView.setApps(mAppsList); + mRecyclerView.setLayoutManager(mLayoutManager); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setHasFixedSize(true); + // No animations will occur when changes occur to the items in this RecyclerView. + mRecyclerView.setItemAnimator(null); + mRecyclerView.addOnScrollListener(mScrollListener); + FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); + mRecyclerView.addItemDecoration(focusedItemDecorator); + mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); + applyPadding(); + } + + void applyPadding() { + if (mRecyclerView != null) { + int bottomOffset = 0; + if (isWork() && mWorkManager.getWorkModeSwitch() != null) { + bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); + } + mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, + mPadding.bottom + bottomOffset); + } + } + + private boolean isWork() { + return mType == WORK; + } + + private boolean isSearch() { + return mType == SEARCH; + } + } } diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java index 85ee636a12..6ecbad24e8 100644 --- a/src/com/android/launcher3/allapps/FloatingHeaderView.java +++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java @@ -30,12 +30,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.R; +import com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.launcher3.views.ActivityContext; import com.android.systemui.plugins.AllAppsRow; import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener; import com.android.systemui.plugins.PluginListener; @@ -54,11 +55,10 @@ public class FloatingHeaderView extends LinearLayout implements private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - } + public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {} @Override - public void onScrolled(RecyclerView rv, int dx, int dy) { + public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) { if (rv != mCurrentRV) { return; } @@ -72,7 +72,8 @@ public class FloatingHeaderView extends LinearLayout implements moved(current); applyVerticalMove(); if (headerCollapsed != mHeaderCollapsed) { - AllAppsContainerView parent = (AllAppsContainerView) getParent(); + BaseAllAppsContainerView parent = + (BaseAllAppsContainerView) getParent(); parent.invalidateHeader(); } } @@ -80,14 +81,16 @@ public class FloatingHeaderView extends LinearLayout implements protected final Map mPluginRows = new ArrayMap<>(); - private final int mHeaderTopPadding; + // These two values are necessary to ensure that the header protection is drawn correctly. + private final int mHeaderTopAdjustment; + private final int mHeaderBottomAdjustment; private final boolean mHeaderProtectionSupported; protected ViewGroup mTabLayout; private AllAppsRecyclerView mMainRV; private AllAppsRecyclerView mWorkRV; + private SearchRecyclerView mSearchRV; private AllAppsRecyclerView mCurrentRV; - private ViewGroup mParent; public boolean mHeaderCollapsed; protected int mSnappedScrolledY; private int mTranslationY; @@ -96,7 +99,6 @@ public class FloatingHeaderView extends LinearLayout implements protected boolean mTabsHidden; protected int mMaxTranslation; - private boolean mMainRVActive = true; private boolean mCollapsed = false; @@ -115,10 +117,14 @@ public class FloatingHeaderView extends LinearLayout implements public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); - mHeaderTopPadding = context.getResources() - .getDimensionPixelSize(R.dimen.all_apps_header_top_padding); + mHeaderTopAdjustment = context.getResources() + .getDimensionPixelSize(R.dimen.all_apps_header_top_adjustment); + mHeaderBottomAdjustment = context.getResources() + .getDimensionPixelSize(R.dimen.all_apps_header_bottom_adjustment); mHeaderProtectionSupported = context.getResources().getBoolean( - R.bool.config_header_protection_supported); + R.bool.config_header_protection_supported) + // TODO(b/208599118) Support header protection for bottom sheet. + && !ActivityContext.lookupContext(context).getDeviceProfile().isTablet; } @Override @@ -154,12 +160,20 @@ public class FloatingHeaderView extends LinearLayout implements @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mMainRV != null) { - mTabLayout.getLayoutParams().width = mMainRV.getTabWidth(); - } + mTabLayout.getLayoutParams().width = getTabWidth(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + /** + * Returns distance between left and right app icons + */ + public int getTabWidth() { + DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); + int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + int iconPadding = totalWidth / grid.numShownAllAppsColumns - grid.allAppsIconSizePx; + return totalWidth - iconPadding - grid.allAppsIconDrawablePaddingPx; + } + private void recreateAllRowsArray() { int pluginCount = mPluginRows.size(); if (pluginCount == 0) { @@ -192,8 +206,8 @@ public class FloatingHeaderView extends LinearLayout implements int oldMaxHeight = mMaxTranslation; updateExpectedHeight(); - if (mMaxTranslation != oldMaxHeight) { - AllAppsContainerView parent = (AllAppsContainerView) getParent(); + if (mMaxTranslation != oldMaxHeight || mCollapsed) { + BaseAllAppsContainerView parent = (BaseAllAppsContainerView) getParent(); if (parent != null) { parent.setupHeader(); } @@ -222,7 +236,8 @@ public class FloatingHeaderView extends LinearLayout implements return super.getFocusedChild(); } - public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) { + void setup(AllAppsRecyclerView mainRV, AllAppsRecyclerView workRV, SearchRecyclerView searchRV, + int activeRV, boolean tabsHidden) { for (FloatingHeaderRow row : mAllRows) { row.setup(this, mAllRows, tabsHidden); } @@ -230,18 +245,27 @@ public class FloatingHeaderView extends LinearLayout implements mTabsHidden = tabsHidden; mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE); - mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView); - mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView); - mParent = (ViewGroup) mMainRV.getParent(); - setMainActive(mMainRVActive || mWorkRV == null); + mMainRV = mainRV; + mWorkRV = workRV; + mSearchRV = searchRV; + setActiveRV(activeRV); reset(false); } - private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) { - if (old != updated && updated != null) { - updated.addOnScrollListener(mOnScrollListener); + /** Whether this header has been set up previously. */ + boolean isSetUp() { + return mMainRV != null; + } + + /** Set the active AllApps RV which will adjust the alpha of the header when scrolled. */ + void setActiveRV(int rvType) { + if (mCurrentRV != null) { + mCurrentRV.removeOnScrollListener(mOnScrollListener); } - return updated; + mCurrentRV = + rvType == AdapterHolder.MAIN ? mMainRV + : rvType == AdapterHolder.WORK ? mWorkRV : mSearchRV; + mCurrentRV.addOnScrollListener(mOnScrollListener); } private void updateExpectedHeight() { @@ -252,11 +276,9 @@ public class FloatingHeaderView extends LinearLayout implements for (FloatingHeaderRow row : mAllRows) { mMaxTranslation += row.getExpectedHeight(); } - } - - public void setMainActive(boolean active) { - mCurrentRV = active ? mMainRV : mWorkRV; - mMainRVActive = active; + if (!mTabsHidden) { + mMaxTranslation += mHeaderBottomAdjustment; + } } public int getMaxTranslation() { @@ -301,7 +323,7 @@ public class FloatingHeaderView extends LinearLayout implements int uncappedTranslationY = mTranslationY; mTranslationY = Math.max(mTranslationY, -mMaxTranslation); - if (mCollapsed || uncappedTranslationY < mTranslationY - mHeaderTopPadding) { + if (mCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) { // we hide it completely if already capped (for opening search anim) for (FloatingHeaderRow row : mAllRows) { row.setVerticalScroll(0, true /* isScrolledOut */); @@ -314,15 +336,23 @@ public class FloatingHeaderView extends LinearLayout implements mTabLayout.setTranslationY(mTranslationY); - int clipHeight = mHeaderTopPadding - getPaddingBottom(); - mRVClip.top = mTabsHidden ? clipHeight : 0; - mHeaderClip.top = clipHeight; + int clipTop = getPaddingTop() - mHeaderTopAdjustment; + if (mTabsHidden) { + clipTop += getPaddingBottom() - mHeaderBottomAdjustment; + } + mRVClip.top = mTabsHidden ? clipTop : 0; + mHeaderClip.top = clipTop; // clipping on a draw might cause additional redraw setClipBounds(mHeaderClip); - mMainRV.setClipBounds(mRVClip); + if (mMainRV != null) { + mMainRV.setClipBounds(mRVClip); + } if (mWorkRV != null) { mWorkRV.setClipBounds(mRVClip); } + if (mSearchRV != null) { + mSearchRV.setClipBounds(mRVClip); + } } /** @@ -389,8 +419,8 @@ public class FloatingHeaderView extends LinearLayout implements } private void calcOffset(Point p) { - p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft(); - p.y = getTop() - mCurrentRV.getTop() - mParent.getTop(); + p.x = getLeft() - mCurrentRV.getLeft() - ((ViewGroup) mCurrentRV.getParent()).getLeft(); + p.y = getTop() - mCurrentRV.getTop() - ((ViewGroup) mCurrentRV.getParent()).getTop(); } public boolean hasVisibleContent() { @@ -413,7 +443,7 @@ public class FloatingHeaderView extends LinearLayout implements @Override public void setInsets(Rect insets) { - DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile(); + DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); for (FloatingHeaderRow row : mAllRows) { row.setInsets(insets, grid); } @@ -444,5 +474,3 @@ public class FloatingHeaderView extends LinearLayout implements return Math.max(getHeight() - getPaddingTop() + mTranslationY, 0); } } - - diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java index b6dcec679b..20f5e7440d 100644 --- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java @@ -16,19 +16,18 @@ package com.android.launcher3.allapps; import android.content.Context; -import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.WindowInsets; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; /** * AllAppsContainerView with launcher specific callbacks */ -public class LauncherAllAppsContainerView extends AllAppsContainerView { - - private final Launcher mLauncher; +public class LauncherAllAppsContainerView extends ActivityAllAppsContainerView { public LauncherAllAppsContainerView(Context context) { this(context, null); @@ -40,14 +39,13 @@ public class LauncherAllAppsContainerView extends AllAppsContainerView { public LauncherAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mLauncher = Launcher.getLauncher(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // The AllAppsContainerView houses the QSB and is hence visible from the Workspace // Overview states. We shouldn't intercept for the scrubber in these cases. - if (!mLauncher.isInState(LauncherState.ALL_APPS)) { + if (!mActivityContext.isInState(LauncherState.ALL_APPS)) { mTouchHandler = null; return false; } @@ -57,22 +55,18 @@ public class LauncherAllAppsContainerView extends AllAppsContainerView { @Override public boolean onTouchEvent(MotionEvent ev) { - if (!mLauncher.isInState(LauncherState.ALL_APPS)) { + if (!mActivityContext.isInState(LauncherState.ALL_APPS)) { return false; } return super.onTouchEvent(ev); } @Override - public void setInsets(Rect insets) { - super.setInsets(insets); - int allAppsStartingPositionY = mLauncher.getDeviceProfile().availableHeightPx - - mLauncher.getDeviceProfile().allAppsOpenVerticalTranslate; - mLauncher.getAllAppsController().setScrollRangeDelta(allAppsStartingPositionY); - } - - @Override - public void onActivePageChanged(int currentActivePage) { - super.onActivePageChanged(currentActivePage); + protected int getNavBarScrimHeight(WindowInsets insets) { + if (Utilities.ATLEAST_Q) { + return insets.getTappableElementInsets().bottom; + } else { + return insets.getStableInsetBottom(); + } } } diff --git a/src/com/android/launcher3/allapps/SearchRecyclerView.java b/src/com/android/launcher3/allapps/SearchRecyclerView.java new file mode 100644 index 0000000000..482bd296f0 --- /dev/null +++ b/src/com/android/launcher3/allapps/SearchRecyclerView.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import android.content.Context; +import android.util.AttributeSet; + +import com.android.launcher3.views.RecyclerViewFastScroller; + +/** A RecyclerView for AllApps Search results. */ +public class SearchRecyclerView extends AllAppsRecyclerView { + private static final String TAG = "SearchRecyclerView"; + + public SearchRecyclerView(Context context) { + this(context, null); + } + + public SearchRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SearchRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void updatePoolSize() { + RecycledViewPool pool = getRecycledViewPool(); + pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, mNumAppsPerRow); + // TODO(b/206905515): Add maxes for other View types. + } + + @Override + public boolean supportsFastScrolling() { + return false; + } + + @Override + public RecyclerViewFastScroller getScrollbar() { + return null; + } +} diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java index 7478b53d55..6138bc4a9d 100644 --- a/src/com/android/launcher3/allapps/SearchUiManager.java +++ b/src/com/android/launcher3/allapps/SearchUiManager.java @@ -29,7 +29,7 @@ public interface SearchUiManager { /** * Initializes the search manager. */ - void initializeSearch(AllAppsContainerView containerView); + void initializeSearch(ActivityAllAppsContainerView containerView); /** * Notifies the search manager to close any active search session. @@ -65,4 +65,12 @@ public interface SearchUiManager { * sets highlight result's title */ default void setFocusedResultTitle(@Nullable CharSequence title) { } + + /** Refresh the currently displayed list of results. */ + default void refreshResults() {} + + /** Returns whether search is in zero state. */ + default boolean inZeroState() { + return false; + } } diff --git a/src/com/android/launcher3/allapps/SecondaryLauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/SecondaryLauncherAllAppsContainerView.java new file mode 100644 index 0000000000..0719c4342f --- /dev/null +++ b/src/com/android/launcher3/allapps/SecondaryLauncherAllAppsContainerView.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import android.content.Context; +import android.util.AttributeSet; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher; + +/** + * AllAppsContainerView for secondary launcher + */ +public class SecondaryLauncherAllAppsContainerView extends + ActivityAllAppsContainerView { + + public SecondaryLauncherAllAppsContainerView(Context context) { + this(context, null); + } + + public SecondaryLauncherAllAppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SecondaryLauncherAllAppsContainerView(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void updateBackground(DeviceProfile deviceProfile) {} +} diff --git a/src/com/android/launcher3/allapps/WorkAdapterProvider.java b/src/com/android/launcher3/allapps/WorkAdapterProvider.java index 331320d6ed..76d08c8084 100644 --- a/src/com/android/launcher3/allapps/WorkAdapterProvider.java +++ b/src/com/android/launcher3/allapps/WorkAdapterProvider.java @@ -17,9 +17,14 @@ package com.android.launcher3.allapps; import android.content.SharedPreferences; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import com.android.launcher3.R; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; +import com.android.launcher3.model.StringCache; +import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; @@ -35,9 +40,11 @@ public class WorkAdapterProvider extends BaseAdapterProvider { @WorkProfileManager.WorkProfileState private int mState; + private ActivityContext mActivityContext; private SharedPreferences mPreferences; - WorkAdapterProvider(SharedPreferences prefs) { + WorkAdapterProvider(ActivityContext activityContext, SharedPreferences prefs) { + mActivityContext = activityContext; mPreferences = prefs; } @@ -53,7 +60,38 @@ public class WorkAdapterProvider extends BaseAdapterProvider { ViewGroup parent, int viewType) { int viewId = viewType == VIEW_TYPE_WORK_DISABLED_CARD ? R.layout.work_apps_paused : R.layout.work_apps_edu; - return new AllAppsGridAdapter.ViewHolder(layoutInflater.inflate(viewId, parent, false)); + View view = layoutInflater.inflate(viewId, parent, false); + setDeviceManagementResources(view, viewType); + return new AllAppsGridAdapter.ViewHolder(view); + } + + private void setDeviceManagementResources(View view, int viewType) { + StringCache cache = mActivityContext.getStringCache(); + if (cache == null) { + return; + } + if (viewType == VIEW_TYPE_WORK_DISABLED_CARD) { + setWorkProfilePausedResources(view, cache); + } else { + setWorkProfileEduResources(view, cache); + } + } + + private void setWorkProfilePausedResources(View view, StringCache cache) { + TextView title = view.findViewById(R.id.work_apps_paused_title); + title.setText(cache.workProfilePausedTitle); + + TextView body = view.findViewById(R.id.work_apps_paused_content); + body.setText(cache.workProfilePausedDescription); + + TextView button = view.findViewById(R.id.enable_work_apps); + button.setText(cache.workProfileEnableButton); + } + + private void setWorkProfileEduResources(View view, StringCache cache) { + TextView title = view.findViewById(R.id.work_apps_paused_title); + title.setText(cache.workProfileEdu); + } /** @@ -69,13 +107,9 @@ public class WorkAdapterProvider extends BaseAdapterProvider { public int addWorkItems(ArrayList adapterItems) { if (mState == WorkProfileManager.STATE_DISABLED) { //add disabled card here. - AllAppsGridAdapter.AdapterItem disabledCard = new AllAppsGridAdapter.AdapterItem(); - disabledCard.viewType = VIEW_TYPE_WORK_DISABLED_CARD; - adapterItems.add(disabledCard); + adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD)); } else if (mState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) { - AllAppsGridAdapter.AdapterItem eduCard = new AllAppsGridAdapter.AdapterItem(); - eduCard.viewType = VIEW_TYPE_WORK_EDU_CARD; - adapterItems.add(eduCard); + adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD)); } return adapterItems.size(); diff --git a/src/com/android/launcher3/allapps/WorkEduCard.java b/src/com/android/launcher3/allapps/WorkEduCard.java index 9db7bf01ac..836cd5af93 100644 --- a/src/com/android/launcher3/allapps/WorkEduCard.java +++ b/src/com/android/launcher3/allapps/WorkEduCard.java @@ -23,16 +23,18 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; -import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.views.ActivityContext; /** * Work profile toggle switch shown at the bottom of AllApps work tab */ -public class WorkEduCard extends FrameLayout implements View.OnClickListener, +public class WorkEduCard extends FrameLayout implements + View.OnClickListener, Animation.AnimationListener { - private final Launcher mLauncher; + private final ActivityContext mActivityContext; Animation mDismissAnim; private int mPosition = -1; @@ -46,7 +48,7 @@ public class WorkEduCard extends FrameLayout implements View.OnClickListener, public WorkEduCard(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mLauncher = Launcher.getLauncher(getContext()); + mActivityContext = ActivityContext.lookupContext(getContext()); mDismissAnim = AnimationUtils.loadAnimation(context, android.R.anim.fade_out); mDismissAnim.setDuration(500); mDismissAnim.setAnimationListener(this); @@ -69,13 +71,14 @@ public class WorkEduCard extends FrameLayout implements View.OnClickListener, super.onFinishInflate(); findViewById(R.id.action_btn).setOnClickListener(this); MarginLayoutParams lp = ((MarginLayoutParams) findViewById(R.id.wrapper).getLayoutParams()); - lp.width = mLauncher.getAppsView().getActiveRecyclerView().getTabWidth(); + lp.width = mActivityContext.getAppsView().getFloatingHeaderView().getTabWidth(); } @Override public void onClick(View view) { startAnimation(mDismissAnim); - mLauncher.getSharedPrefs().edit().putInt(WorkAdapterProvider.KEY_WORK_EDU_STEP, 1).apply(); + Utilities.getPrefs(getContext()).edit().putInt(WorkAdapterProvider.KEY_WORK_EDU_STEP, + 1).apply(); } @Override @@ -97,8 +100,8 @@ public class WorkEduCard extends FrameLayout implements View.OnClickListener, if (mPosition == -1) { if (getParent() != null) ((ViewGroup) getParent()).removeView(WorkEduCard.this); } else { - AllAppsRecyclerView rv = mLauncher.getAppsView() - .mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView; + AllAppsRecyclerView rv = mActivityContext.getAppsView().mAH.get( + ActivityAllAppsContainerView.AdapterHolder.WORK).mRecyclerView; rv.getApps().getAdapterItems().remove(mPosition); rv.getAdapter().notifyItemRemoved(mPosition); } diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java index be015817d0..733577eabf 100644 --- a/src/com/android/launcher3/allapps/WorkModeSwitch.java +++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java @@ -26,12 +26,12 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.Button; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; -import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; import com.android.launcher3.anim.KeyboardInsetAnimationCallback; +import com.android.launcher3.model.StringCache; +import com.android.launcher3.views.ActivityContext; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; /** @@ -73,8 +73,14 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi new KeyboardInsetAnimationCallback(this); setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback); } - DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile(); + ActivityContext activityContext = ActivityContext.lookupContext(getContext()); + DeviceProfile grid = activityContext.getDeviceProfile(); setInsets(grid.getInsets()); + + StringCache cache = activityContext.getStringCache(); + if (cache != null) { + setText(cache.workProfilePauseButton); + } } @Override @@ -91,7 +97,7 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi @Override public void onActivePageChanged(int page) { - mOnWorkTab = page == AllAppsContainerView.AdapterHolder.WORK; + mOnWorkTab = page == ActivityAllAppsContainerView.AdapterHolder.WORK; updateVisibility(); } @@ -99,9 +105,9 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi public void onClick(View view) { if (Utilities.ATLEAST_P && isEnabled()) { setFlag(FLAG_PROFILE_TOGGLE_ONGOING); - Launcher launcher = Launcher.getLauncher(getContext()); - launcher.getStatsLogManager().logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); - launcher.getAppsView().getWorkManager().setWorkProfileEnabled(false); + ActivityContext activityContext = ActivityContext.lookupContext(getContext()); + activityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); + activityContext.getAppsView().getWorkManager().setWorkProfileEnabled(false); } } @@ -121,7 +127,6 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi } } - private void updateVisibility() { clearAnimation(); if (mWorkEnabled && mOnWorkTab) { diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java index 7593ca7427..729622f519 100644 --- a/src/com/android/launcher3/allapps/WorkPausedCard.java +++ b/src/com/android/launcher3/allapps/WorkPausedCard.java @@ -24,16 +24,16 @@ import android.view.View; import android.widget.Button; import android.widget.LinearLayout; -import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.views.ActivityContext; /** * Work profile toggle switch shown at the bottom of AllApps work tab */ public class WorkPausedCard extends LinearLayout implements View.OnClickListener { - private final Launcher mLauncher; + private final ActivityContext mActivityContext; private Button mBtn; public WorkPausedCard(Context context) { @@ -46,7 +46,7 @@ public class WorkPausedCard extends LinearLayout implements View.OnClickListener public WorkPausedCard(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mLauncher = Launcher.getLauncher(getContext()); + mActivityContext = ActivityContext.lookupContext(getContext()); } @@ -61,8 +61,8 @@ public class WorkPausedCard extends LinearLayout implements View.OnClickListener public void onClick(View view) { if (Utilities.ATLEAST_P) { setEnabled(false); - mLauncher.getAppsView().getWorkManager().setWorkProfileEnabled(true); - mLauncher.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP); + mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true); + mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP); } } diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java index e223248866..b70cb13b07 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -26,20 +26,25 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.function.Predicate; /** - * Companion class for {@link AllAppsContainerView} to manage work tab and personal tab related + * Companion class for {@link BaseAllAppsContainerView} to manage work tab and personal tab + * related * logic based on {@link WorkProfileState}? */ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener { @@ -50,7 +55,6 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP public static final int STATE_DISABLED = 2; public static final int STATE_TRANSITION = 3; - private final UserManager mUserManager; /** @@ -65,21 +69,23 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP public @interface WorkProfileState { } - private final AllAppsContainerView mAllApps; + private final BaseAllAppsContainerView mAllApps; private final WorkAdapterProvider mAdapterProvider; - private final ItemInfoMatcher mMatcher; + private final Predicate mMatcher; private WorkModeSwitch mWorkModeSwitch; + private final DeviceProfile mDeviceProfile; @WorkProfileState private int mCurrentState; - public WorkProfileManager(UserManager userManager, AllAppsContainerView allApps, - SharedPreferences preferences) { + public WorkProfileManager(UserManager userManager, BaseAllAppsContainerView allApps, + SharedPreferences preferences, DeviceProfile deviceProfile) { mUserManager = userManager; mAllApps = allApps; - mAdapterProvider = new WorkAdapterProvider(preferences); + mDeviceProfile = deviceProfile; + mAdapterProvider = new WorkAdapterProvider(allApps.mActivityContext, preferences); mMatcher = mAllApps.mPersonalMatcher.negate(); } @@ -118,7 +124,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mCurrentState = currentState; mAdapterProvider.updateCurrentState(currentState); if (getAH() != null) { - getAH().appsList.updateAdapterItems(); + getAH().mAppsList.updateAdapterItems(); } if (mWorkModeSwitch != null) { mWorkModeSwitch.updateCurrentState(currentState == STATE_ENABLED); @@ -126,7 +132,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } /** - * Creates and attaches for profile toggle button to {@link AllAppsContainerView} + * Creates and attaches for profile toggle button to {@link BaseAllAppsContainerView} */ public boolean attachWorkModeSwitch() { if (!mAllApps.getAppsStore().hasModelFlag( @@ -138,6 +144,24 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate( R.layout.work_mode_fab, mAllApps, false); } + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mWorkModeSwitch.getLayoutParams(); + int workFabMarginBottom = + mWorkModeSwitch.getResources().getDimensionPixelSize( + R.dimen.work_fab_margin_bottom); + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + workFabMarginBottom <<= 1; // Double margin to add space above search bar. + workFabMarginBottom += + mWorkModeSwitch.getResources().getDimensionPixelSize(R.dimen.qsb_widget_height); + } + if (!mAllApps.mActivityContext.getDeviceProfile().isGestureMode){ + workFabMarginBottom += mAllApps.mActivityContext.getDeviceProfile().getInsets().bottom; + } + lp.bottomMargin = workFabMarginBottom; + int totalScreenWidth = mDeviceProfile.widthPx; + int personalWorkTabWidth = + mAllApps.mActivityContext.getAppsView().getFloatingHeaderView().getTabWidth(); + lp.rightMargin = lp.leftMargin = (totalScreenWidth - personalWorkTabWidth) / 2; if (mWorkModeSwitch.getParent() != mAllApps) { mAllApps.addView(mWorkModeSwitch); } @@ -147,9 +171,8 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mWorkModeSwitch.updateCurrentState(mCurrentState == STATE_ENABLED); return true; } - /** - * Removes work profile toggle button from {@link AllAppsContainerView} + * Removes work profile toggle button from {@link BaseAllAppsContainerView} */ public void detachWorkModeSwitch() { if (mWorkModeSwitch != null && mWorkModeSwitch.getParent() == mAllApps) { @@ -158,12 +181,11 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mWorkModeSwitch = null; } - public WorkAdapterProvider getAdapterProvider() { return mAdapterProvider; } - public ItemInfoMatcher getMatcher() { + public Predicate getMatcher() { return mMatcher; } @@ -172,8 +194,8 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP return mWorkModeSwitch; } - private AllAppsContainerView.AdapterHolder getAH() { - return mAllApps.mAH[AllAppsContainerView.AdapterHolder.WORK]; + private BaseAllAppsContainerView.AdapterHolder getAH() { + return mAllApps.mAH.get(BaseAllAppsContainerView.AdapterHolder.WORK); } public int getCurrentState() { diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java index 0137e2a2c0..886460e5c7 100644 --- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java +++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java @@ -16,6 +16,7 @@ package com.android.launcher3.allapps.search; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_QUICK_SEARCH_WITH_IME; import android.text.Editable; import android.text.SpannableStringBuilder; @@ -29,14 +30,13 @@ import android.view.inputmethod.EditorInfo; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.ExtendedEditText; -import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; -import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.search.SearchAlgorithm; import com.android.launcher3.search.SearchCallback; +import com.android.launcher3.views.ActivityContext; /** * An interface to a search box that AllApps can command. @@ -45,7 +45,7 @@ public class AllAppsSearchBarController implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener, OnFocusChangeListener { - protected BaseDraggingActivity mLauncher; + protected ActivityContext mLauncher; protected SearchCallback mCallback; protected ExtendedEditText mInput; protected String mQuery; @@ -62,7 +62,7 @@ public class AllAppsSearchBarController */ public final void initialize( SearchAlgorithm searchAlgorithm, ExtendedEditText input, - BaseDraggingActivity launcher, SearchCallback callback) { + ActivityContext launcher, SearchCallback callback) { mCallback = callback; mLauncher = launcher; @@ -123,9 +123,11 @@ public class AllAppsSearchBarController if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) { mLauncher.getStatsLogManager().logger() - .log(LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME); + .log(actionId == EditorInfo.IME_ACTION_SEARCH + ? LAUNCHER_ALLAPPS_QUICK_SEARCH_WITH_IME + : LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME); // selectFocusedView should return SearchTargetEvent that is passed onto onClick - return Launcher.getLauncher(mLauncher).getAppsView().launchHighlightedItem(); + return mLauncher.getAppsView().getMainAdapterProvider().launchHighlightedItem(); } return false; } diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java index 4c5a9e64c9..6539c05d84 100644 --- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java +++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java @@ -32,17 +32,16 @@ import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Insettable; import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsContainerView; -import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsStore; -import com.android.launcher3.allapps.AlphabeticalAppsList; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.allapps.SearchUiManager; import com.android.launcher3.search.SearchCallback; +import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; @@ -53,12 +52,11 @@ public class AppsSearchContainerLayout extends ExtendedEditText implements SearchUiManager, SearchCallback, AllAppsStore.OnUpdateListener, Insettable { - private final BaseDraggingActivity mLauncher; + private final ActivityContext mLauncher; private final AllAppsSearchBarController mSearchBarController; private final SpannableStringBuilder mSearchQueryBuilder; - private AlphabeticalAppsList mApps; - private AllAppsContainerView mAppsView; + private ActivityAllAppsContainerView mAppsView; // The amount of pixels to shift down and overlap with the rest of the content. private final int mContentOverlap; @@ -74,7 +72,7 @@ public class AppsSearchContainerLayout extends ExtendedEditText public AppsSearchContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mLauncher = BaseDraggingActivity.fromContext(context); + mLauncher = ActivityContext.lookupContext(context); mSearchBarController = new AllAppsSearchBarController(); mSearchQueryBuilder = new SpannableStringBuilder(); @@ -82,7 +80,7 @@ public class AppsSearchContainerLayout extends ExtendedEditText setHint(prefixTextWithIcon(getContext(), R.drawable.ic_allapps_search, getHint())); mContentOverlap = - getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_field_height) / 2; + getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_content_overlap); } @Override @@ -130,11 +128,10 @@ public class AppsSearchContainerLayout extends ExtendedEditText } @Override - public void initializeSearch(AllAppsContainerView appsView) { - mApps = appsView.getApps(); + public void initializeSearch(ActivityAllAppsContainerView appsView) { mAppsView = appsView; mSearchBarController.initialize( - new DefaultAppSearchAlgorithm(mLauncher), + new DefaultAppSearchAlgorithm(getContext()), this, mLauncher, this); } @@ -170,25 +167,14 @@ public class AppsSearchContainerLayout extends ExtendedEditText @Override public void onSearchResult(String query, ArrayList items) { if (items != null) { - mApps.setSearchResults(items); - notifyResultChanged(); + mAppsView.setSearchResults(items); mAppsView.setLastSearchQuery(query); } } - @Override - public void onAppendSearchResult(String query, ArrayList items) { - if (items != null) { - mApps.appendSearchResults(items); - notifyResultChanged(); - } - } - @Override public void clearSearchResult() { - if (mApps.setSearchResults(null)) { - notifyResultChanged(); - } + mAppsView.setSearchResults(null); // Clear the search query mSearchQueryBuilder.clear(); @@ -197,10 +183,6 @@ public class AppsSearchContainerLayout extends ExtendedEditText mAppsView.onClearSearchResult(); } - private void notifyResultChanged() { - mAppsView.onSearchResultsChanged(); - } - @Override public void setInsets(Rect insets) { MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java index 1f854c6787..4eceb7184e 100644 --- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java +++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java @@ -23,7 +23,7 @@ import android.os.Handler; import androidx.annotation.AnyThread; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.model.AllAppsList; import com.android.launcher3.model.BaseModelUpdateTask; import com.android.launcher3.model.BgDataModel; @@ -85,8 +85,7 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { for (int i = 0; i < total && resultCount < MAX_RESULTS_COUNT; i++) { AppInfo info = apps.get(i); if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) { - AdapterItem appItem = AdapterItem.asApp(resultCount, "", info, resultCount); - result.add(appItem); + result.add(AdapterItem.asApp(info)); resultCount++; } } diff --git a/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java index 7abd555b9d..a95bd514da 100644 --- a/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java +++ b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java @@ -23,23 +23,21 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.BubbleTextView; -import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsGridAdapter; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.views.AppLauncher; /** - * Provides views for local search results + * Provides views for local search results. */ -public class DefaultSearchAdapterProvider extends SearchAdapterProvider { +public class DefaultSearchAdapterProvider extends SearchAdapterProvider { private final RecyclerView.ItemDecoration mDecoration; private View mHighlightedView; - public DefaultSearchAdapterProvider(BaseDraggingActivity launcher, - AllAppsContainerView appsContainerView) { - super(launcher, appsContainerView); + public DefaultSearchAdapterProvider(AppLauncher launcher) { + super(launcher); mDecoration = new RecyclerView.ItemDecoration() { @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, diff --git a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java index 7af0406e30..bc52784caf 100644 --- a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java +++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java @@ -21,18 +21,19 @@ import android.view.View; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.BaseAdapterProvider; +import com.android.launcher3.views.ActivityContext; /** * A UI expansion wrapper providing for search results + * + * @param Context for this adapter provider. */ -public abstract class SearchAdapterProvider extends BaseAdapterProvider { +public abstract class SearchAdapterProvider extends BaseAdapterProvider { - protected final BaseDraggingActivity mLauncher; + protected final T mLauncher; - public SearchAdapterProvider(BaseDraggingActivity launcher, AllAppsContainerView appsView) { + public SearchAdapterProvider(T launcher) { mLauncher = launcher; } diff --git a/src/com/android/launcher3/anim/AnimatedPropertySetter.java b/src/com/android/launcher3/anim/AnimatedPropertySetter.java new file mode 100644 index 0000000000..e5f5e7c44b --- /dev/null +++ b/src/com/android/launcher3/anim/AnimatedPropertySetter.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.anim; + +import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.graphics.drawable.ColorDrawable; +import android.util.FloatProperty; +import android.util.IntProperty; +import android.view.View; + +import androidx.annotation.NonNull; + +import java.util.function.Consumer; + +/** + * Extension of {@link PropertySetter} which applies the property through an animation + */ +public class AnimatedPropertySetter extends PropertySetter { + + protected final AnimatorSet mAnim = new AnimatorSet(); + protected ValueAnimator mProgressAnimator; + + @Override + public Animator setViewAlpha(View view, float alpha, TimeInterpolator interpolator) { + if (view == null || view.getAlpha() == alpha) { + return NO_OP; + } + ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha); + anim.addListener(new AlphaUpdateListener(view)); + anim.setInterpolator(interpolator); + add(anim); + return anim; + } + + @Override + public Animator setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) { + if (view == null || (view.getBackground() instanceof ColorDrawable + && ((ColorDrawable) view.getBackground()).getColor() == color)) { + return NO_OP; + } + ObjectAnimator anim = ObjectAnimator.ofArgb(view, VIEW_BACKGROUND_COLOR, color); + anim.setInterpolator(interpolator); + add(anim); + return anim; + } + + @Override + public Animator setFloat(T target, FloatProperty property, float value, + TimeInterpolator interpolator) { + if (property.get(target) == value) { + return NO_OP; + } + Animator anim = ObjectAnimator.ofFloat(target, property, value); + anim.setInterpolator(interpolator); + add(anim); + return anim; + } + + @Override + public Animator setInt(T target, IntProperty property, int value, + TimeInterpolator interpolator) { + if (property.get(target) == value) { + return NO_OP; + } + Animator anim = ObjectAnimator.ofInt(target, property, value); + anim.setInterpolator(interpolator); + add(anim); + return anim; + } + + + /** + * Adds a callback to be run on every frame of the animation + */ + public void addOnFrameCallback(Runnable runnable) { + addOnFrameListener(anim -> runnable.run()); + } + + /** + * Adds a listener to be run on every frame of the animation + */ + public void addOnFrameListener(ValueAnimator.AnimatorUpdateListener listener) { + if (mProgressAnimator == null) { + mProgressAnimator = ValueAnimator.ofFloat(0, 1); + } + + mProgressAnimator.addUpdateListener(listener); + } + + @Override + public void addEndListener(Consumer listener) { + if (mProgressAnimator == null) { + mProgressAnimator = ValueAnimator.ofFloat(0, 1); + } + mProgressAnimator.addListener(AnimatorListeners.forEndCallback(listener)); + } + + /** + * @see AnimatorSet#addListener(AnimatorListener) + */ + public void addListener(Animator.AnimatorListener listener) { + mAnim.addListener(listener); + } + + @Override + public void add(Animator a) { + mAnim.play(a); + } + + /** + * Creates and returns the underlying AnimatorSet + */ + @NonNull + public AnimatorSet buildAnim() { + // Add progress animation to the end, so that frame callback is called after all the other + // animation update. + if (mProgressAnimator != null) { + add(mProgressAnimator); + mProgressAnimator = null; + } + return mAnim; + } +} diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java index 85ca280ba2..1cc0c21745 100644 --- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java +++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java @@ -19,7 +19,7 @@ import static com.android.launcher3.Utilities.boundToRange; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.clampToProgress; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import android.animation.Animator; import android.animation.Animator.AnimatorListener; diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java index 1e7b2247b8..0a77aa7bcf 100644 --- a/src/com/android/launcher3/anim/Interpolators.java +++ b/src/com/android/launcher3/anim/Interpolators.java @@ -57,6 +57,11 @@ public class Interpolators { public static final Interpolator DECELERATED_EASE = new PathInterpolator(0, 0, .2f, 1f); public static final Interpolator ACCELERATED_EASE = new PathInterpolator(0.4f, 0, 1f, 1f); + public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 0.8f, 0.15f); + public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( + 0.05f, 0.7f, 0.1f, 1f); + public static final Interpolator EXAGGERATED_EASE; public static final Interpolator INSTANT = t -> 1; @@ -145,8 +150,9 @@ public class Interpolators { } /** - * Runs the given interpolator such that the entire progress is set between the given bounds. - * That is, we set the interpolation to 0 until lowerBound and reach 1 by upperBound. + * Returns a function that runs the given interpolator such that the entire progress is set + * between the given bounds. That is, we set the interpolation to 0 until lowerBound and reach + * 1 by upperBound. */ public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound, float upperBound) { @@ -155,18 +161,42 @@ public class Interpolators { String.format("upperBound (%f) must be greater than lowerBound (%f)", upperBound, lowerBound)); } - return t -> { - if (t == lowerBound && t == upperBound) { - return t == 0f ? 0 : 1; - } - if (t < lowerBound) { - return 0; - } - if (t > upperBound) { - return 1; - } - return interpolator.getInterpolation((t - lowerBound) / (upperBound - lowerBound)); - }; + return t -> clampToProgress(interpolator, t, lowerBound, upperBound); + } + + /** + * Returns the progress value's progress between the lower and upper bounds. That is, the + * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound. + * + * Between lowerBound and upperBound, the progress value will be interpolated using the provided + * interpolator. + */ + public static float clampToProgress( + Interpolator interpolator, float progress, float lowerBound, float upperBound) { + if (upperBound < lowerBound) { + throw new IllegalArgumentException( + String.format("upperBound (%f) must be greater than lowerBound (%f)", + upperBound, lowerBound)); + } + + if (progress == lowerBound && progress == upperBound) { + return progress == 0f ? 0 : 1; + } + if (progress < lowerBound) { + return 0; + } + if (progress > upperBound) { + return 1; + } + return interpolator.getInterpolation((progress - lowerBound) / (upperBound - lowerBound)); + } + + /** + * Returns the progress value's progress between the lower and upper bounds. That is, the + * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound. + */ + public static float clampToProgress(float progress, float lowerBound, float upperBound) { + return clampToProgress(Interpolators.LINEAR, progress, lowerBound, upperBound); } /** @@ -178,4 +208,14 @@ public class Interpolators { float upperBound) { return t -> Utilities.mapRange(interpolator.getInterpolation(t), lowerBound, upperBound); } + + /** + * Returns the reverse of the provided interpolator, following the formula: g(x) = 1 - f(1 - x). + * In practice, this means that if f is an interpolator used to model a value animating between + * m and n, g is the interpolator to use to obtain the specular behavior when animating from n + * to m. + */ + public static Interpolator reverse(Interpolator interpolator) { + return t -> 1 - interpolator.getInterpolation(1 - t); + } } diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java index 3ab893b0a1..7316420b12 100644 --- a/src/com/android/launcher3/anim/PendingAnimation.java +++ b/src/com/android/launcher3/anim/PendingAnimation.java @@ -15,24 +15,18 @@ */ package com.android.launcher3.anim; -import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.graphics.drawable.ColorDrawable; import android.util.FloatProperty; -import android.util.IntProperty; -import android.view.View; import com.android.launcher3.anim.AnimatorPlaybackController.Holder; import java.util.ArrayList; -import java.util.function.Consumer; /** * Utility class to keep track of a running animation. @@ -43,17 +37,13 @@ import java.util.function.Consumer; * * TODO: Find a better name */ -public class PendingAnimation implements PropertySetter { +public class PendingAnimation extends AnimatedPropertySetter { private final ArrayList mAnimHolders = new ArrayList<>(); - private final AnimatorSet mAnim; private final long mDuration; - private ValueAnimator mProgressAnimator; - public PendingAnimation(long duration) { mDuration = duration; - mAnim = new AnimatorSet(); } public long getDuration() { @@ -68,6 +58,7 @@ public class PendingAnimation implements PropertySetter { add(anim, springProperty); } + @Override public void add(Animator anim) { add(anim, SpringProperty.DEFAULT); } @@ -77,37 +68,11 @@ public class PendingAnimation implements PropertySetter { addAnimationHoldersRecur(a, mDuration, springProperty, mAnimHolders); } - @Override - public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) { - if (view == null || view.getAlpha() == alpha) { - return; - } - ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha); - anim.addListener(new AlphaUpdateListener(view)); - anim.setInterpolator(interpolator); - add(anim); - } - - @Override - public void setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) { - if (view == null || (view.getBackground() instanceof ColorDrawable - && ((ColorDrawable) view.getBackground()).getColor() == color)) { - return; - } - ObjectAnimator anim = ObjectAnimator.ofArgb(view, VIEW_BACKGROUND_COLOR, color); - anim.setInterpolator(interpolator); - add(anim); - } - - @Override - public void setFloat(T target, FloatProperty property, float value, - TimeInterpolator interpolator) { - if (property.get(target) == value) { - return; - } - Animator anim = ObjectAnimator.ofFloat(target, property, value); - anim.setDuration(mDuration).setInterpolator(interpolator); - add(anim); + /** + * Configures interpolator of the underlying AnimatorSet. + */ + public void setInterpolator(TimeInterpolator interpolator) { + mAnim.setInterpolator(interpolator); } public void addFloat(T target, FloatProperty property, float from, float to, @@ -117,57 +82,16 @@ public class PendingAnimation implements PropertySetter { add(anim); } - @Override - public void setInt(T target, IntProperty property, int value, - TimeInterpolator interpolator) { - if (property.get(target) == value) { - return; - } - Animator anim = ObjectAnimator.ofInt(target, property, value); - anim.setInterpolator(interpolator); - add(anim); - } - - /** - * Adds a callback to be run on every frame of the animation - */ - public void addOnFrameCallback(Runnable runnable) { - addOnFrameListener(anim -> runnable.run()); - } - - /** - * Adds a listener to be run on every frame of the animation - */ - public void addOnFrameListener(ValueAnimator.AnimatorUpdateListener listener) { - if (mProgressAnimator == null) { - mProgressAnimator = ValueAnimator.ofFloat(0, 1); - } - - mProgressAnimator.addUpdateListener(listener); - } - - /** - * @see AnimatorSet#addListener(AnimatorListener) - */ - public void addListener(Animator.AnimatorListener listener) { - mAnim.addListener(listener); - } - /** * Creates and returns the underlying AnimatorSet */ + @Override public AnimatorSet buildAnim() { - // Add progress animation to the end, so that frame callback is called after all the other - // animation update. - if (mProgressAnimator != null) { - add(mProgressAnimator); - mProgressAnimator = null; - } if (mAnimHolders.isEmpty()) { // Add a placeholder animation to that the duration is respected add(ValueAnimator.ofFloat(0, 1).setDuration(mDuration)); } - return mAnim; + return super.buildAnim(); } /** @@ -176,14 +100,4 @@ public class PendingAnimation implements PropertySetter { public AnimatorPlaybackController createPlaybackController() { return new AnimatorPlaybackController(buildAnim(), mDuration, mAnimHolders); } - - /** - * Add a listener of receiving the success/failure callback in the end. - */ - public void addEndListener(Consumer listener) { - if (mProgressAnimator == null) { - mProgressAnimator = ValueAnimator.ofFloat(0, 1); - } - mProgressAnimator.addListener(AnimatorListeners.forEndCallback(listener)); - } } diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java index 8d77b4ba8d..d2207f6351 100644 --- a/src/com/android/launcher3/anim/PropertySetter.java +++ b/src/com/android/launcher3/anim/PropertySetter.java @@ -17,57 +17,94 @@ package com.android.launcher3.anim; import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.util.FloatProperty; import android.util.IntProperty; import android.view.View; +import androidx.annotation.NonNull; + +import java.util.function.Consumer; + /** * Utility class for setting a property with or without animation */ -public interface PropertySetter { +public abstract class PropertySetter { - PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter() { }; + public static final PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter() { + + @Override + public void add(Animator animatorSet) { + animatorSet.setDuration(0); + animatorSet.start(); + } + }; + + protected static final AnimatorSet NO_OP = new AnimatorSet(); /** * Sets the view alpha using the provided interpolator. * Unlike {@link #setFloat}, this also updates the visibility of the view as alpha changes * between zero and non-zero. */ - default void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) { + @NonNull + public Animator setViewAlpha(View view, float alpha, TimeInterpolator interpolator) { if (view != null) { view.setAlpha(alpha); AlphaUpdateListener.updateVisibility(view); } + return NO_OP; } /** * Sets the background color of the provided view using the provided interpolator. */ - default void setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) { + @NonNull + public Animator setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) { if (view != null) { view.setBackgroundColor(color); } + return NO_OP; } /** * Updates the float property of the target using the provided interpolator */ - default void setFloat(T target, FloatProperty property, float value, + @NonNull + public Animator setFloat(T target, FloatProperty property, float value, TimeInterpolator interpolator) { property.setValue(target, value); + return NO_OP; } /** * Updates the int property of the target using the provided interpolator */ - default void setInt(T target, IntProperty property, int value, + @NonNull + public Animator setInt(T target, IntProperty property, int value, TimeInterpolator interpolator) { property.setValue(target, value); + return NO_OP; } - default void add(Animator animatorSet) { - animatorSet.setDuration(0); - animatorSet.start(); + /** + * Runs the animation as part of setting the property + */ + public abstract void add(Animator animatorSet); + + /** + * Add a listener of receiving the success/failure callback in the end. + */ + public void addEndListener(Consumer listener) { + listener.accept(true); + } + + /** + * Creates and returns the AnimatorSet that can be run to apply the properties + */ + @NonNull + public AnimatorSet buildAnim() { + return NO_OP; } } diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java index bd52158f0a..40fa0cfd02 100644 --- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java +++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java @@ -25,7 +25,7 @@ import android.util.FloatProperty; import androidx.annotation.FloatRange; import androidx.dynamicanimation.animation.SpringForce; -import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.window.RefreshRateTracker; /** * Utility class to build an object animator which follows the same path as a spring animation for @@ -134,7 +134,7 @@ public class SpringAnimationBuilder { } public SpringAnimationBuilder computeParams() { - int singleFrameMs = DisplayController.getSingleFrameMs(mContext); + int singleFrameMs = RefreshRateTracker.getSingleFrameMs(mContext); double naturalFreq = Math.sqrt(mStiffness); double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio); diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java index 46c9006ddf..4f8d53e11c 100644 --- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -4,12 +4,12 @@ import android.content.Context; import android.icu.text.AlphabeticIndex; import android.os.LocaleList; +import androidx.annotation.NonNull; + import com.android.launcher3.Utilities; import java.util.Locale; -import androidx.annotation.NonNull; - public class AlphabeticIndexCompat { private static final String MID_DOT = "\u2219"; diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 2d31aa4687..33beacd4c0 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -52,7 +52,7 @@ public final class FeatureFlags { * Enable moving the QSB on the 0th screen of the workspace. This is not a configuration feature * and should be modified at a project level. */ - public static final boolean QSB_ON_FIRST_SCREEN = true; + public static final boolean QSB_ON_FIRST_SCREEN = BuildConfig.QSB_ON_FIRST_SCREEN; /** * Feature flag to handle define config changes dynamically instead of killing the process. @@ -79,9 +79,6 @@ public final class FeatureFlags { public static final BooleanFlag KEYGUARD_ANIMATION = getDebugFlag( "KEYGUARD_ANIMATION", false, "Enable animation for keyguard going away on wallpaper"); - public static final BooleanFlag ADAPTIVE_ICON_WINDOW_ANIM = getDebugFlag( - "ADAPTIVE_ICON_WINDOW_ANIM", true, "Use adaptive icons for window animations."); - public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag( "ENABLE_QUICKSTEP_LIVE_TILE", true, "Enable live tile in Quickstep overview"); @@ -92,11 +89,21 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag( "ENABLE_DEVICE_SEARCH", true, "Allows on device search in all apps"); + public static final BooleanFlag ENABLE_FLOATING_SEARCH_BAR = + getDebugFlag("ENABLE_FLOATING_SEARCH_BAR", false, + "Keep All Apps search bar at the bottom (but above keyboard if open)"); + + public static final BooleanFlag ENABLE_QUICK_SEARCH = new DeviceFlag("ENABLE_QUICK_SEARCH", + true, "Use quick search behavior."); + + public static final BooleanFlag COLLECT_SEARCH_HISTORY = new DeviceFlag( + "COLLECT_SEARCH_HISTORY", false, "Allow launcher to collect search history for log"); + public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag( "ENABLE_TWOLINE_ALLAPPS", false, "Enables two line label inside all apps."); public static final BooleanFlag ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING = new DeviceFlag( - "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", true, + "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", false, "Allows on device search in all apps logging"); public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag( @@ -132,12 +139,12 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_BULK_WORKSPACE_ICON_LOADING = getDebugFlag( "ENABLE_BULK_WORKSPACE_ICON_LOADING", - false, + true, "Enable loading workspace icons in bulk."); public static final BooleanFlag ENABLE_BULK_ALL_APPS_ICON_LOADING = getDebugFlag( "ENABLE_BULK_ALL_APPS_ICON_LOADING", - false, + true, "Enable loading all apps icons in bulk."); // Keep as DeviceFlag for remote disable in emergency. @@ -190,20 +197,10 @@ public final class FeatureFlags { "ENABLE_APP_PREDICTIONS_WHILE_VISIBLE", true, "Allows app " + "predictions to be updated while they are visible to the user."); - public static final BooleanFlag ENABLE_TASKBAR = getDebugFlag( - "ENABLE_TASKBAR", true, "Allows a system Taskbar to be shown on larger devices."); - - public static final BooleanFlag ENABLE_TASKBAR_EDU = getDebugFlag("ENABLE_TASKBAR_EDU", true, - "Enables showing taskbar education the first time an app is opened."); - public static final BooleanFlag ENABLE_TASKBAR_POPUP_MENU = getDebugFlag( - "ENABLE_TASKBAR_POPUP_MENU", false, "Enables long pressing taskbar icons to show the" + "ENABLE_TASKBAR_POPUP_MENU", true, "Enables long pressing taskbar icons to show the" + " popup menu."); - public static final BooleanFlag ENABLE_OVERVIEW_GRID = getDebugFlag( - "ENABLE_OVERVIEW_GRID", true, "Uses grid overview layout. " - + "Only applicable on large screen devices."); - public static final BooleanFlag ENABLE_TWO_PANEL_HOME = getDebugFlag( "ENABLE_TWO_PANEL_HOME", true, "Uses two panel on home screen. Only applicable on large screen devices."); @@ -245,6 +242,41 @@ public final class FeatureFlags { "ENABLE_ICON_LABEL_AUTO_SCALING", true, "Enables scaling/spacing for icon labels to make more characters visible"); + public static final BooleanFlag ENABLE_ALL_APPS_IN_TASKBAR = getDebugFlag( + "ENABLE_ALL_APPS_IN_TASKBAR", true, + "Enables accessing All Apps from the system Taskbar."); + + public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag( + "ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT", false, + "Enables displaying the all apps button in the hotseat."); + + public static final BooleanFlag ENABLE_ALL_APPS_ONE_SEARCH_IN_TASKBAR = getDebugFlag( + "ENABLE_ALL_APPS_ONE_SEARCH_IN_TASKBAR", false, + "Enables One Search box in Taskbar All Apps."); + + public static final BooleanFlag ENABLE_SPLIT_FROM_WORKSPACE = getDebugFlag( + "ENABLE_SPLIT_FROM_WORKSPACE", true, + "Enable initiating split screen from workspace."); + + public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag( + "ENABLE_NEW_MIGRATION_LOGIC", true, + "Enable the new grid migration logic, keeping pages when src < dest"); + + public static final BooleanFlag ENABLE_ONE_SEARCH_MOTION = new DeviceFlag( + "ENABLE_ONE_SEARCH_MOTION", true, "Enables animations in OneSearch."); + + public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = new DeviceFlag( + "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", true, + "Enable option to show keyboard when going to all-apps"); + + public static final BooleanFlag USE_LOCAL_ICON_OVERRIDES = getDebugFlag( + "USE_LOCAL_ICON_OVERRIDES", true, + "Use inbuilt monochrome icons if app doesn't provide one"); + + public static final BooleanFlag ENABLE_DISMISS_PREDICTION_UNDO = getDebugFlag( + "ENABLE_DISMISS_PREDICTION_UNDO", false, + "Show an 'Undo' snackbar when users dismiss a predicted hotseat item"); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index fdb27998de..35cdfef465 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -18,7 +18,6 @@ package com.android.launcher3.dragndrop; import static com.android.launcher3.Utilities.ATLEAST_Q; -import android.content.ComponentName; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -36,12 +35,12 @@ import com.android.launcher3.logging.InstanceId; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.testing.TestProtocol; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; import java.util.Optional; +import java.util.function.Predicate; /** * Class for initiating a drag within a view or across multiple views. @@ -275,15 +274,12 @@ public abstract class DragController protected abstract void exitDrag(); - public void onAppsRemoved(ItemInfoMatcher matcher) { + public void onAppsRemoved(Predicate matcher) { // Cancel the current drag if we are removing an app that we are dragging if (mDragObject != null) { ItemInfo dragInfo = mDragObject.dragInfo; - if (dragInfo instanceof WorkspaceItemInfo) { - ComponentName cn = dragInfo.getTargetComponent(); - if (cn != null && matcher.matches(dragInfo, cn)) { - cancelDrag(); - } + if (dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo)) { + cancelDrag(); } } } diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index 5ee42037b0..8eeca7d6d8 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -65,9 +65,7 @@ import java.util.ArrayList; public class DragLayer extends BaseDragLayer { public static final int ALPHA_INDEX_OVERLAY = 0; - public static final int ALPHA_INDEX_LAUNCHER_LOAD = 1; - public static final int ALPHA_INDEX_TRANSITIONS = 2; - private static final int ALPHA_CHANNEL_COUNT = 3; + private static final int ALPHA_CHANNEL_COUNT = 1; public static final int ANIMATION_END_DISAPPEAR = 0; public static final int ANIMATION_END_REMAIN_VISIBLE = 2; @@ -104,7 +102,10 @@ public class DragLayer extends BaseDragLayer { mFocusIndicatorHelper = new ViewGroupFocusHelper(this); } - public void setup(DragController dragController, Workspace workspace) { + /** + * Set up the drag layer with the parameters. + */ + public void setup(DragController dragController, Workspace workspace) { mDragController = dragController; recreateControllers(); mWorkspaceDragScrim = new Scrim(this); diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index c37613fc5a..0264ae21be 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -21,6 +21,7 @@ import static android.view.View.MeasureSpec.makeMeasureSpec; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.Utilities.getBadge; +import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.animation.Animator; @@ -31,9 +32,9 @@ import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorFilter; import android.graphics.Path; import android.graphics.Picture; import android.graphics.Point; @@ -97,6 +98,7 @@ public abstract class DragView extends Fram final ValueAnimator mAnim; // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends. private boolean mAnimStarted; + private Runnable mOnAnimEndCallback = null; private int mLastTouchX; private int mLastTouchY; @@ -179,6 +181,14 @@ public abstract class DragView extends Fram public void onAnimationStart(Animator animation) { mAnimStarted = true; } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mOnAnimEndCallback != null) { + mOnAnimEndCallback.run(); + } + } }); setDragRegion(new Rect(0, 0, width, height)); @@ -198,6 +208,10 @@ public abstract class DragView extends Fram setWillNotDraw(false); } + public void setOnAnimationEndCallback(Runnable callback) { + mOnAnimEndCallback = callback; + } + /** * Initialize {@code #mIconDrawable} if the item can be represented using * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}. @@ -215,7 +229,8 @@ public abstract class DragView extends Fram Object[] outObj = new Object[1]; int w = mWidth; int h = mHeight; - Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, outObj); + Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, + true /* shouldThemeIcon */, outObj); if (dr instanceof AdaptiveIconDrawable) { int blurMargin = (int) mActivity.getResources() @@ -225,9 +240,8 @@ public abstract class DragView extends Fram bounds.inset(blurMargin, blurMargin); // Badge is applied after icon normalization so the bounds for badge should not // be scaled down due to icon normalization. - Rect badgeBounds = new Rect(bounds); mBadge = getBadge(mActivity, info, outObj[0]); - mBadge.setBounds(badgeBounds); + FastBitmapDrawable.setBadgeBounds(mBadge, bounds); // Do not draw the background in case of folder as its translucent final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon); @@ -280,11 +294,10 @@ public abstract class DragView extends Fram removeAllViewsInLayout(); if (info.isDisabled()) { - FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); - d.setIsDisabled(true); - mBgSpringDrawable.setColorFilter(d.getColorFilter()); - mFgSpringDrawable.setColorFilter(d.getColorFilter()); - mBadge.setColorFilter(d.getColorFilter()); + ColorFilter filter = getDisabledColorFilter(); + mBgSpringDrawable.setColorFilter(filter); + mFgSpringDrawable.setColorFilter(filter); + mBadge.setColorFilter(filter); } invalidate(); })); diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java index 74d9a228e2..6f295e6c50 100644 --- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java +++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java @@ -20,9 +20,13 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.annotation.TargetApi; import android.graphics.Bitmap; -import android.graphics.Matrix; +import android.graphics.Canvas; +import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Picture; +import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; @@ -31,10 +35,11 @@ import android.os.Build; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import com.android.launcher3.Utilities; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.folder.PreviewBackground; -import com.android.launcher3.graphics.ShiftedBitmapDrawable; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.util.Preconditions; import com.android.launcher3.views.ActivityContext; @@ -69,79 +74,104 @@ public class FolderAdaptiveIcon extends AdaptiveIconDrawable { return mBadge; } + @TargetApi(Build.VERSION_CODES.P) public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon( - ActivityContext activity, int folderId, Point dragViewSize) { + ActivityContext activity, int folderId, Point size) { Preconditions.assertNonUiThread(); + if (!Utilities.ATLEAST_P) { + return null; + } - // Create the actual drawable on the UI thread to avoid race conditions with + // assume square + if (size.x != size.y) { + return null; + } + int requestedSize = size.x; + + // Only use the size actually needed for drawing the folder icon + int drawingSize = activity.getDeviceProfile().folderIconSizePx; + int foregroundSize = Math.max(requestedSize, drawingSize); + float shift = foregroundSize - requestedSize; + + Picture background = new Picture(); + Picture foreground = new Picture(); + Picture badge = new Picture(); + + Canvas bgCanvas = background.beginRecording(requestedSize, requestedSize); + Canvas badgeCanvas = badge.beginRecording(requestedSize, requestedSize); + + Canvas fgCanvas = foreground.beginRecording(foregroundSize, foregroundSize); + fgCanvas.translate(shift, shift); + + // Do not clip the folder drawing since the icon previews extend outside the background. + Path mask = new Path(); + mask.addRect(-shift, -shift, requestedSize + shift, requestedSize + shift, + Direction.CCW); + + // Initialize the actual draw commands on the UI thread to avoid race conditions with // FolderIcon draw pass try { - return MAIN_EXECUTOR.submit(() -> { + MAIN_EXECUTOR.submit(() -> { FolderIcon icon = activity.findFolderIcon(folderId); - return icon == null ? null : createDrawableOnUiThread(icon, dragViewSize); - + if (icon == null) { + throw new IllegalArgumentException("Folder not found with id: " + folderId); + } + initLayersOnUiThread(icon, requestedSize, bgCanvas, fgCanvas, badgeCanvas); }).get(); } catch (Exception e) { Log.e(TAG, "Unable to create folder icon", e); return null; + } finally { + background.endRecording(); + foreground.endRecording(); + badge.endRecording(); } + + // Only convert foreground to a bitmap as it can contain multiple draw commands. Other + // layers either draw a nothing or a single draw call. + Bitmap fgBitmap = Bitmap.createBitmap(foreground); + Paint foregroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + // Do not use PictureDrawable as it moves the picture to the canvas bounds, whereas we want + // to draw it at (0,0) + return new FolderAdaptiveIcon( + new BitmapRendererDrawable(c -> c.drawPicture(background)), + new BitmapRendererDrawable( + c -> c.drawBitmap(fgBitmap, -shift, -shift, foregroundPaint)), + new BitmapRendererDrawable(c -> c.drawPicture(badge)), + mask); } - private static FolderAdaptiveIcon createDrawableOnUiThread(FolderIcon icon, - Point dragViewSize) { - Preconditions.assertUIThread(); - + @UiThread + private static void initLayersOnUiThread(FolderIcon icon, int size, + Canvas backgroundCanvas, Canvas foregroundCanvas, Canvas badgeCanvas) { icon.getPreviewBounds(sTmpRect); - - PreviewBackground bg = icon.getFolderBackground(); - - // assume square - assert (dragViewSize.x == dragViewSize.y); final int previewSize = sTmpRect.width(); - final int margin = (dragViewSize.x - previewSize) / 2; + PreviewBackground bg = icon.getFolderBackground(); + final int margin = (size - previewSize) / 2; final float previewShiftX = -sTmpRect.left + margin; final float previewShiftY = -sTmpRect.top + margin; // Initialize badge, which consists of the outline stroke, shadow and dot; these // must be rendered above the foreground - Bitmap badgeBmp = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y, - (canvas) -> { - canvas.save(); - canvas.translate(previewShiftX, previewShiftY); - bg.drawShadow(canvas); - bg.drawBackgroundStroke(canvas); - icon.drawDot(canvas); - canvas.restore(); - }); + badgeCanvas.save(); + badgeCanvas.translate(previewShiftX, previewShiftY); + icon.drawDot(badgeCanvas); + badgeCanvas.restore(); - // Initialize mask - Path mask = new Path(); - Matrix m = new Matrix(); - m.setTranslate(previewShiftX, previewShiftY); - bg.getClipPath().transform(m, mask); + // Draw foreground + foregroundCanvas.save(); + foregroundCanvas.translate(previewShiftX, previewShiftY); + icon.getPreviewItemManager().draw(foregroundCanvas); + foregroundCanvas.restore(); - Bitmap previewBitmap = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y, - (canvas) -> { - canvas.save(); - canvas.translate(previewShiftX, previewShiftY); - icon.getPreviewItemManager().draw(canvas); - canvas.restore(); - }); - - Bitmap bgBitmap = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y, - (canvas) -> { - Paint p = new Paint(); - p.setColor(bg.getBgColor()); - - canvas.drawCircle(dragViewSize.x / 2f, dragViewSize.y / 2f, bg.getRadius(), p); - }); - - ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBmp, 0, 0); - ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap, 0, 0); - ShiftedBitmapDrawable background = new ShiftedBitmapDrawable(bgBitmap, 0, 0); - - return new FolderAdaptiveIcon(background, foreground, badge, mask); + // Draw background + Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setColor(bg.getBgColor()); + bg.drawShadow(backgroundCanvas); + backgroundCanvas.drawCircle(size / 2f, size / 2f, bg.getRadius(), backgroundPaint); + bg.drawBackgroundStroke(backgroundCanvas); } @Override @@ -174,4 +204,52 @@ public class FolderAdaptiveIcon extends AdaptiveIconDrawable { & mBadge.getChangingConfigurations(); } } + + private static class BitmapRendererDrawable extends Drawable { + + private final BitmapRenderer mRenderer; + + BitmapRendererDrawable(BitmapRenderer renderer) { + mRenderer = renderer; + } + + @Override + public void draw(Canvas canvas) { + mRenderer.draw(canvas); + } + + @Override + public void setAlpha(int i) { } + + @Override + public void setColorFilter(ColorFilter colorFilter) { } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public ConstantState getConstantState() { + return new MyConstantState(mRenderer); + } + + private static class MyConstantState extends ConstantState { + private final BitmapRenderer mRenderer; + + MyConstantState(BitmapRenderer renderer) { + mRenderer = renderer; + } + + @Override + public Drawable newDrawable() { + return new BitmapRendererDrawable(mRenderer); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } + } } diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java index 29e7c1854d..f9916d0b3b 100644 --- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java +++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java @@ -87,7 +87,8 @@ class PinShortcutRequestActivityInfo extends ShortcutConfigActivityInfo { // Total duration for the drop animation to complete. long duration = mContext.getResources().getInteger(R.integer.config_dropAnimMaxDuration) + LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY + - LauncherState.SPRING_LOADED.getTransitionDuration(Launcher.getLauncher(mContext)); + LauncherState.SPRING_LOADED.getTransitionDuration(Launcher.getLauncher(mContext), + true /* isToState */); // Delay the actual accept() call until the drop animation is complete. return PinRequestHelper.createWorkspaceItemFromPinItemRequest( mContext, mRequest, duration); diff --git a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java index 6325877936..fb8a1bc99e 100644 --- a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java +++ b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java @@ -55,7 +55,7 @@ public class SpringLoadedDragController implements OnAlarmListener { public void onAlarm(Alarm alarm) { if (mScreen != null) { // Snap to the screen that we are hovering over now - Workspace w = mLauncher.getWorkspace(); + Workspace w = mLauncher.getWorkspace(); if (!w.isVisible(mScreen)) { w.snapToPage(w.indexOfChild(mScreen)); } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index daef68226d..e68ebdb6e5 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -24,7 +24,7 @@ import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustom import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -57,6 +57,7 @@ import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; import android.widget.TextView; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; @@ -71,7 +72,6 @@ import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.OnAlarmListener; -import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Utilities; @@ -87,10 +87,10 @@ import com.android.launcher3.logger.LauncherAtom.FromState; import com.android.launcher3.logger.LauncherAtom.ToState; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; -import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.FolderInfo.FolderListener; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pageindicators.PageIndicatorDots; import com.android.launcher3.util.Executors; @@ -101,6 +101,8 @@ import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.views.ClipPathView; import com.android.launcher3.widget.PendingAddShortcutInfo; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -130,16 +132,19 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo */ private static final int MIN_CONTENT_DIMEN = 5; - static final int STATE_NONE = -1; - static final int STATE_SMALL = 0; - static final int STATE_ANIMATING = 1; - static final int STATE_OPEN = 2; + public static final int STATE_CLOSED = 0; + public static final int STATE_ANIMATING = 1; + public static final int STATE_OPEN = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN}) + public @interface FolderState {} /** * Time for which the scroll hint is shown before automatically changing page. */ public static final int SCROLL_HINT_DURATION = 500; - public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; + private static final int RESCROLL_EXTRA_DELAY = 150; public static final int SCROLL_NONE = -1; public static final int SCROLL_LEFT = 0; @@ -198,13 +203,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @ViewDebug.ExportedProperty(category = "launcher", mapping = { - @ViewDebug.IntToString(from = STATE_NONE, to = "STATE_NONE"), - @ViewDebug.IntToString(from = STATE_SMALL, to = "STATE_SMALL"), + @ViewDebug.IntToString(from = STATE_CLOSED, to = "STATE_CLOSED"), @ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"), @ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"), }) - @Thunk - int mState = STATE_NONE; + private int mState = STATE_CLOSED; + private OnFolderStateChangedListener mOnFolderStateChangedListener; @ViewDebug.ExportedProperty(category = "launcher") private boolean mRearrangeOnClose = false; boolean mItemsInvalidated = false; @@ -277,19 +281,15 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mPageIndicator = findViewById(R.id.folder_page_indicator); mFolderName = findViewById(R.id.folder_name); mFolderName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.folderLabelTextSizePx); - if (mActivityContext.supportsIme()) { - mFolderName.setOnBackKeyListener(this); - mFolderName.setOnFocusChangeListener(this); - mFolderName.setOnEditorActionListener(this); - mFolderName.setSelectAllOnFocus(true); - mFolderName.setInputType(mFolderName.getInputType() - & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT - | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - | InputType.TYPE_TEXT_FLAG_CAP_WORDS); - mFolderName.forceDisableSuggestions(true); - } else { - mFolderName.setEnabled(false); - } + mFolderName.setOnBackKeyListener(this); + mFolderName.setOnFocusChangeListener(this); + mFolderName.setOnEditorActionListener(this); + mFolderName.setSelectAllOnFocus(true); + mFolderName.setInputType(mFolderName.getInputType() + & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + mFolderName.forceDisableSuggestions(true); mFooter = findViewById(R.id.folder_footer); mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height); @@ -561,7 +561,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - mState = STATE_ANIMATING; + setState(STATE_ANIMATING); mCurrentAnimator = a; } @@ -686,7 +686,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Override public void onAnimationEnd(Animator animation) { - mState = STATE_OPEN; + setState(STATE_OPEN); announceAccessibilityChanges(); AccessibilityManagerCompat.sendFolderOpenedEventToTest(getContext()); @@ -862,7 +862,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } mSuppressFolderDeletion = false; clearDragInfo(); - mState = STATE_SMALL; + setState(STATE_CLOSED); mContent.setCurrentPage(0); } @@ -1282,9 +1282,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo final WorkspaceItemInfo si; if (pasiSi != null) { si = pasiSi; - } else if (d.dragInfo instanceof AppInfo) { + } else if (d.dragInfo instanceof WorkspaceItemFactory) { // Came from all apps -- make a copy. - si = ((AppInfo) d.dragInfo).makeWorkspaceItem(); + si = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(launcher); } else { // WorkspaceItemInfo si = (WorkspaceItemInfo) d.dragInfo; @@ -1522,7 +1522,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // Pause drag event until the scrolling is finished mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); - mScrollPauseAlarm.setAlarm(RESCROLL_DELAY); + int rescrollDelay = getResources().getInteger( + R.integer.config_pageSnapAnimationDuration) + RESCROLL_EXTRA_DELAY; + mScrollPauseAlarm.setAlarm(rescrollDelay); } } @@ -1655,4 +1657,21 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return windowBottomPx - folderBottomPx; } + + private void setState(@FolderState int newState) { + mState = newState; + if (mOnFolderStateChangedListener != null) { + mOnFolderStateChangedListener.onFolderStateChanged(mState); + } + } + + public void setOnFolderStateChangedListener(@Nullable OnFolderStateChangedListener listener) { + mOnFolderStateChangedListener = listener; + } + + /** Listener that can be registered via {@link Folder#setOnFolderStateChangedListener} */ + public interface OnFolderStateChangedListener { + /** See {@link Folder.FolderState} */ + void onFolderStateChanged(@FolderState int newState); + } } diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 98be72a6b9..b1e27019c3 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -55,7 +55,7 @@ import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dot.FolderDotInfo; @@ -68,11 +68,11 @@ import com.android.launcher3.logger.LauncherAtom.FromState; import com.android.launcher3.logger.LauncherAtom.ToState; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.FolderInfo.FolderListener; import com.android.launcher3.model.data.FolderInfo.LabelState; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.touch.ItemClickHandler; import com.android.launcher3.util.Executors; @@ -284,7 +284,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel mBackground.animateToAccept(cl, lp.cellX, lp.cellY); mOpenAlarm.setOnAlarmListener(mOnOpenListener); if (SPRING_LOADING_ENABLED && - ((dragInfo instanceof AppInfo) + ((dragInfo instanceof WorkspaceItemFactory) || (dragInfo instanceof WorkspaceItemInfo) || (dragInfo instanceof PendingAddShortcutInfo))) { mOpenAlarm.setAlarm(ON_OPEN_DELAY); @@ -342,7 +342,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel Rect to = finalRect; if (to == null) { to = new Rect(); - Workspace workspace = launcher.getWorkspace(); + Workspace workspace = launcher.getWorkspace(); // Set cellLayout and this to it's final state to compute final animation locations workspace.setFinalTransitionTransform(); float scaleX = getScaleX(); @@ -397,7 +397,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel float finalScale = scale * scaleRelativeToDragLayer; // Account for potentially different icon sizes with non-default grid settings - if (d.dragSource instanceof AllAppsContainerView) { + if (d.dragSource instanceof ActivityAllAppsContainerView) { DeviceProfile grid = mActivity.getDeviceProfile(); float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx); finalScale *= containerScale; @@ -486,9 +486,9 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { WorkspaceItemInfo item; - if (d.dragInfo instanceof AppInfo) { + if (d.dragInfo instanceof WorkspaceItemFactory) { // Came from all apps -- make a copy - item = ((AppInfo) d.dragInfo).makeWorkspaceItem(); + item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext()); } else if (d.dragSource instanceof BaseItemDragListener){ // Came from a different window -- make a copy item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); @@ -637,7 +637,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel // If we are animating to the accepting state, animate the dot out. mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress()); - mDotParams.color = mBackground.getDotColor(); + mDotParams.dotColor = mBackground.getDotColor(); mDotRenderer.draw(canvas, mDotParams); } } diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java index 9c1b24d96c..502164473f 100644 --- a/src/com/android/launcher3/folder/FolderNameProvider.java +++ b/src/com/android/launcher3/folder/FolderNameProvider.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.folder; +import android.annotation.SuppressLint; +import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.os.Process; @@ -22,11 +24,15 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.WorkerThread; + import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.model.AllAppsList; import com.android.launcher3.model.BaseModelUpdateTask; import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -94,6 +100,7 @@ public class FolderNameProvider implements ResourceBasedOverride { /** * Generate and rank the suggested Folder names. */ + @WorkerThread public void getSuggestedFolderName(Context context, ArrayList workspaceItemInfos, FolderNameInfos nameInfos) { @@ -101,13 +108,13 @@ public class FolderNameProvider implements ResourceBasedOverride { if (DEBUG) { Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString()); } + // If all the icons are from work profile, // Then, suggest "Work" as the folder name Set users = workspaceItemInfos.stream().map(w -> w.user) .collect(Collectors.toSet()); if (users.size() == 1 && !users.contains(Process.myUserHandle())) { - setAsLastSuggestion(nameInfos, - context.getResources().getString(R.string.work_folder_name)); + setAsLastSuggestion(nameInfos, getWorkFolderName(context)); } // If all the icons are from same package (e.g., main icon, shortcut, shortcut) @@ -121,13 +128,25 @@ public class FolderNameProvider implements ResourceBasedOverride { if (packageNames.size() == 1) { Optional info = getAppInfoByPackageName(packageNames.iterator().next()); // Place it as first viable suggestion and shift everything else - info.ifPresent(i -> setAsFirstSuggestion(nameInfos, i.title.toString())); + info.ifPresent(i -> setAsFirstSuggestion( + nameInfos, i.title == null ? "" : i.title.toString())); } if (DEBUG) { Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString()); } } + @WorkerThread + @SuppressLint("NewApi") + private String getWorkFolderName(Context context) { + if (!Utilities.ATLEAST_T) { + return context.getString(R.string.work_folder_name); + } + return context.getSystemService(DevicePolicyManager.class).getResources() + .getString(StringCache.WORK_FOLDER_NAME, () -> + context.getString(R.string.work_folder_name)); + } + private Optional getAppInfoByPackageName(String packageName) { if (mAppInfos == null || mAppInfos.isEmpty()) { return Optional.empty(); diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java index 65991e48b2..3d5aef5f0b 100644 --- a/src/com/android/launcher3/folder/FolderPagedView.java +++ b/src/com/android/launcher3/folder/FolderPagedView.java @@ -252,7 +252,7 @@ public class FolderPagedView extends PagedView implements Cli } @Override - protected int getChildGap() { + protected int getChildGap(int fromIndex, int toIndex) { return getPaddingLeft() + getPaddingRight(); } diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java index c5b3913a2b..1f0a011b39 100644 --- a/src/com/android/launcher3/folder/LauncherDelegate.java +++ b/src/com/android/launcher3/folder/LauncherDelegate.java @@ -100,7 +100,8 @@ public class LauncherDelegate { } // Remove the folder - mLauncher.removeItem(folder.mFolderIcon, info, true /* deleteFromDb */); + mLauncher.removeItem(folder.mFolderIcon, info, true /* deleteFromDb */, + "folder removed because there's only 1 item in it"); if (folder.mFolderIcon instanceof DropTarget) { folder.mDragController.removeDropTarget((DropTarget) folder.mFolderIcon); } diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java index 8bef6ad7ff..6355b62e27 100644 --- a/src/com/android/launcher3/folder/PreviewItemManager.java +++ b/src/com/android/launcher3/folder/PreviewItemManager.java @@ -21,6 +21,7 @@ import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDE import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; +import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -429,7 +430,7 @@ public class PreviewItemManager { drawable.setLevel(item.getProgressLevel()); p.drawable = drawable; } else { - p.drawable = item.newIcon(mContext, true); + p.drawable = item.newIcon(mContext, FLAG_THEMED); } p.drawable.setBounds(0, 0, mIconSize, mIconSize); p.item = item; diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java index 3d4a1001a2..778b32a863 100644 --- a/src/com/android/launcher3/graphics/IconPalette.java +++ b/src/com/android/launcher3/graphics/IconPalette.java @@ -16,18 +16,16 @@ package com.android.launcher3.graphics; -import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; - import android.app.Notification; import android.content.Context; import android.graphics.Color; import android.util.Log; +import androidx.core.graphics.ColorUtils; + import com.android.launcher3.R; import com.android.launcher3.util.Themes; -import androidx.core.graphics.ColorUtils; - /** * Contains colors based on the dominant color of an icon. */ @@ -147,9 +145,4 @@ public class IconPalette { } return ColorUtils.LABToColor(low, a, b); } - - public static int getMutedColor(int color, float whiteScrimAlpha) { - int whiteScrim = setColorAlphaBound(Color.WHITE, (int) (255 * whiteScrimAlpha)); - return ColorUtils.compositeColors(whiteScrim, color); - } } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 73e18f42f9..d5bcb0cbcb 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -23,7 +23,6 @@ import static android.view.View.VISIBLE; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks; -import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially; import android.annotation.TargetApi; import android.app.Fragment; @@ -43,7 +42,6 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.os.Process; import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.ContextThemeWrapper; @@ -85,9 +83,11 @@ import com.android.launcher3.pm.UserCache; import com.android.launcher3.uioverrides.PredictedAppIconInflater; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.widget.BaseLauncherAppWidgetHostView; @@ -112,7 +112,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; * 3) Place appropriate elements like icons and first-page qsb * 4) Measure and draw the view on a canvas */ -@TargetApi(Build.VERSION_CODES.O) +@TargetApi(Build.VERSION_CODES.R) public class LauncherPreviewRenderer extends ContextWrapper implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 { @@ -129,28 +129,31 @@ public class LauncherPreviewRenderer extends ContextWrapper public PreviewContext(Context base, InvariantDeviceProfile idp) { super(base, UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, - CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE); + CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE, + WindowManagerProxy.INSTANCE, DisplayController.INSTANCE); mIdp = idp; mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp); mObjectMap.put(LauncherAppState.INSTANCE, new LauncherAppState(this, null /* iconCacheFileName */)); - } - public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) { + /** + * Creates a new LauncherIcons for the preview, skipping the global pool + */ + public LauncherIcons newLauncherIcons(Context context) { LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll(); if (launcherIconsForPreview != null) { return launcherIconsForPreview; } return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize, - -1 /* poolId */, shapeDetection); + -1 /* poolId */); } private final class LauncherIconsForPreview extends LauncherIcons { private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize, - int poolId, boolean shapeDetection) { - super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection); + int poolId) { + super(context, fillResIconDpi, iconBitmapSize, poolId); } @Override @@ -185,27 +188,21 @@ public class LauncherPreviewRenderer extends ContextWrapper mIdp = idp; mDp = idp.getDeviceProfile(context).copy(context); - if (Utilities.ATLEAST_R) { - WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class) - .getCurrentWindowMetrics().getWindowInsets(); - mInsets = new Rect( - currentWindowInsets.getSystemWindowInsetLeft(), - currentWindowInsets.getSystemWindowInsetTop(), - currentWindowInsets.getSystemWindowInsetRight(), - currentWindowInsets.getSystemWindowInsetBottom()); - } else { - mInsets = new Rect(); - mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2; - mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2; - } + WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class) + .getCurrentWindowMetrics().getWindowInsets(); + mInsets = new Rect( + currentWindowInsets.getSystemWindowInsetLeft(), + currentWindowInsets.getSystemWindowInsetTop(), + currentWindowInsets.getSystemWindowInsetRight(), + mDp.isTaskbarPresent ? 0 : currentWindowInsets.getSystemWindowInsetBottom()); mDp.updateInsets(mInsets); BaseIconFactory iconFactory = new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { }; - BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable( - new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)), - Process.myUserHandle(), - Build.VERSION.SDK_INT); + BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap( + new AdaptiveIconDrawable( + new ColorDrawable(Color.WHITE), + new ColorDrawable(Color.WHITE))); mWorkspaceItemInfo = new WorkspaceItemInfo(); mWorkspaceItemInfo.bitmap = iconInfo; @@ -228,21 +225,21 @@ public class LauncherPreviewRenderer extends ContextWrapper mHotseat.resetLayout(false); CellLayout firstScreen = mRootView.findViewById(R.id.workspace); - firstScreen.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx, - mDp.workspacePadding.top, + firstScreen.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingPx.left, + mDp.workspacePadding.top + mDp.cellLayoutPaddingPx.top, (mDp.isTwoPanels ? mDp.cellLayoutBorderSpacePx.x / 2 - : mDp.workspacePadding.right) + mDp.cellLayoutPaddingLeftRightPx, - mDp.workspacePadding.bottom + : mDp.workspacePadding.right) + mDp.cellLayoutPaddingPx.right, + mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom ); mWorkspaceScreens.put(FIRST_SCREEN_ID, firstScreen); if (mDp.isTwoPanels) { CellLayout rightPanel = mRootView.findViewById(R.id.workspace_right); rightPanel.setPadding( - mDp.cellLayoutBorderSpacePx.x / 2 + mDp.cellLayoutPaddingLeftRightPx, - mDp.workspacePadding.top, - mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx, - mDp.workspacePadding.bottom + mDp.cellLayoutBorderSpacePx.x / 2 + mDp.cellLayoutPaddingPx.left, + mDp.workspacePadding.top + mDp.cellLayoutPaddingPx.top, + mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right, + mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom ); mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel); } @@ -423,8 +420,6 @@ public class LauncherPreviewRenderer extends ContextWrapper currentWorkspaceItems, otherWorkspaceItems); filterCurrentWorkspaceItems(currentScreenIds, dataModel.appWidgets, currentAppWidgets, otherAppWidgets); - - sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems); for (ItemInfo itemInfo : currentWorkspaceItems) { switch (itemInfo.itemType) { case Favorites.ITEM_TYPE_APPLICATION: @@ -457,10 +452,10 @@ public class LauncherPreviewRenderer extends ContextWrapper } IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems, mDp.numShownHotseatIcons); - FixedContainerItems hotseatpredictions = + FixedContainerItems hotseatPredictions = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION); - List predictions = hotseatpredictions == null - ? Collections.emptyList() : hotseatpredictions.items; + List predictions = hotseatPredictions == null + ? Collections.emptyList() : hotseatPredictions.items; int count = Math.min(ranks.size(), predictions.size()); for (int i = 0; i < count; i++) { int rank = ranks.get(i); diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java index 24d6fe5fc1..d2e4c511f9 100644 --- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -345,11 +345,10 @@ public class PreloadIconDrawable extends FastBitmapDrawable { } @Override - public ConstantState getConstantState() { + public FastBitmapConstantState newConstantState() { return new PreloadIconConstantState( mBitmap, mIconColor, - !mItem.isAppStartable(), mItem, mIndicatorColor, new int[] {mSystemAccentColor, mSystemBackgroundColor}, @@ -367,12 +366,11 @@ public class PreloadIconDrawable extends FastBitmapDrawable { public PreloadIconConstantState( Bitmap bitmap, int iconColor, - boolean isDisabled, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode) { - super(bitmap, iconColor, isDisabled); + super(bitmap, iconColor); mInfo = info; mIndicatorColor = indicatorColor; mPreloadColors = preloadColors; @@ -381,17 +379,12 @@ public class PreloadIconDrawable extends FastBitmapDrawable { } @Override - public PreloadIconDrawable newDrawable() { + public PreloadIconDrawable createDrawable() { return new PreloadIconDrawable( mInfo, mIndicatorColor, mPreloadColors, mIsDarkMode); } - - @Override - public int getChangingConfigurations() { - return 0; - } } } diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index 2f3d5d8a61..fd11b37795 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -47,7 +47,6 @@ import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.GridSizeMigrationTaskV2; import com.android.launcher3.model.LoaderTask; -import com.android.launcher3.model.ModelDelegate; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.Themes; @@ -156,9 +155,10 @@ public class PreviewSurfaceRenderer { PreviewContext previewContext = new PreviewContext(inflationContext, mIdp); new LoaderTask( LauncherAppState.getInstance(previewContext), - null, + /* bgAllAppsList= */ null, new BgDataModel(), - new ModelDelegate(), null) { + LauncherAppState.getInstance(previewContext).getModel().getModelDelegate(), + /* results= */ null) { @Override public void run() { diff --git a/src/com/android/launcher3/graphics/ShiftedBitmapDrawable.java b/src/com/android/launcher3/graphics/ShiftedBitmapDrawable.java deleted file mode 100644 index f8583b89e0..0000000000 --- a/src/com/android/launcher3/graphics/ShiftedBitmapDrawable.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.graphics; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.drawable.Drawable; - -/** - * A simple drawable which draws a bitmap at a fixed position irrespective of the bounds - */ -public class ShiftedBitmapDrawable extends Drawable { - - private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); - private final Bitmap mBitmap; - private float mShiftX; - private float mShiftY; - - private final ConstantState mConstantState; - - public ShiftedBitmapDrawable(Bitmap bitmap, float shiftX, float shiftY) { - mBitmap = bitmap; - mShiftX = shiftX; - mShiftY = shiftY; - - mConstantState = new MyConstantState(mBitmap, mShiftX, mShiftY); - } - - public float getShiftX() { - return mShiftX; - } - - public float getShiftY() { - return mShiftY; - } - - public void setShiftX(float shiftX) { - mShiftX = shiftX; - } - - public void setShiftY(float shiftY) { - mShiftY = shiftY; - } - - @Override - public void draw(Canvas canvas) { - canvas.drawBitmap(mBitmap, mShiftX, mShiftY, mPaint); - } - - @Override - public void setAlpha(int i) { } - - @Override - public void setColorFilter(ColorFilter colorFilter) { - mPaint.setColorFilter(colorFilter); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public ConstantState getConstantState() { - return mConstantState; - } - - private static class MyConstantState extends ConstantState { - private final Bitmap mBitmap; - private float mShiftX; - private float mShiftY; - - MyConstantState(Bitmap bitmap, float shiftX, float shiftY) { - mBitmap = bitmap; - mShiftX = shiftX; - mShiftY = shiftY; - } - - @Override - public Drawable newDrawable() { - return new ShiftedBitmapDrawable(mBitmap, mShiftX, mShiftY); - } - - @Override - public int getChangingConfigurations() { - return 0; - } - } -} \ No newline at end of file diff --git a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java index 248a57d2fd..c8606b1002 100644 --- a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java +++ b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.icons.BaseIconFactory.IconOptions; /** * Extension of ComponentWithLabel to also support loading icons @@ -47,7 +48,7 @@ public interface ComponentWithLabelAndIcon extends ComponentWithLabel { return super.loadIcon(context, object); } try (LauncherIcons li = LauncherIcons.obtain(context)) { - return li.createBadgedIconBitmap(d, object.getUser(), 0); + return li.createBadgedIconBitmap(d, new IconOptions().setUser(object.getUser())); } } } diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 936eeb9220..fe9b633b66 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -16,6 +16,7 @@ package com.android.launcher3.icons; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY; @@ -41,9 +42,11 @@ import android.os.Trace; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; +import android.util.SparseArray; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherFiles; @@ -93,6 +96,8 @@ public class IconCache extends BaseIconCache { private final InstantAppResolver mInstantAppResolver; private final IconProvider mIconProvider; + private final SparseArray mWidgetCategoryBitmapInfos; + private int mPendingIconRequestCount = 0; public IconCache(Context context, InvariantDeviceProfile idp) { @@ -110,6 +115,7 @@ public class IconCache extends BaseIconCache { mUserManager = UserCache.INSTANCE.get(mContext); mInstantAppResolver = InstantAppResolver.newInstance(mContext); mIconProvider = iconProvider; + mWidgetCategoryBitmapInfos = new SparseArray<>(); } @Override @@ -216,14 +222,7 @@ public class IconCache extends BaseIconCache { * Fill in {@param info} with the icon for {@param si} */ public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) { - getShortcutIcon(info, si, true, mIsUsingFallbackOrNonDefaultIconCheck); - } - - /** - * Fill in {@param info} with an unbadged icon for {@param si} - */ - public void getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) { - getShortcutIcon(info, si, false, mIsUsingFallbackOrNonDefaultIconCheck); + getShortcutIcon(info, si, mIsUsingFallbackOrNonDefaultIconCheck); } /** @@ -232,11 +231,6 @@ public class IconCache extends BaseIconCache { */ public void getShortcutIcon(T info, ShortcutInfo si, @NonNull Predicate fallbackIconCheck) { - getShortcutIcon(info, si, true /* use badged */, fallbackIconCheck); - } - - private synchronized void getShortcutIcon(T info, ShortcutInfo si, - boolean useBadged, @NonNull Predicate fallbackIconCheck) { BitmapInfo bitmapInfo; if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) { bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName, si.getUserHandle(), @@ -252,13 +246,7 @@ public class IconCache extends BaseIconCache { if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) { return; } - info.bitmap = bitmapInfo; - if (useBadged) { - BitmapInfo badgeInfo = getShortcutInfoBadge(si); - try (LauncherIcons li = LauncherIcons.obtain(mContext)) { - info.bitmap = li.badgeBitmap(info.bitmap.icon, badgeInfo); - } - } + info.bitmap = bitmapInfo.withBadgeInfo(getShortcutInfoBadge(si)); } /** @@ -357,6 +345,17 @@ public class IconCache extends BaseIconCache { List> iconRequestInfos) { Map, List>> iconLoadSubsectionsMap = iconRequestInfos.stream() + .filter(iconRequest -> { + if (iconRequest.itemInfo.getTargetComponent() == null) { + Log.i(TAG, + "Skipping Item info with null component name: " + + iconRequest.itemInfo); + iconRequest.itemInfo.bitmap = getDefaultIcon( + iconRequest.itemInfo.user); + return false; + } + return true; + }) .collect(groupingBy(iconRequest -> Pair.create(iconRequest.itemInfo.user, iconRequest.useLowResIcon))); @@ -364,45 +363,116 @@ public class IconCache extends BaseIconCache { iconLoadSubsectionsMap.forEach((sectionKey, filteredList) -> { Map>> duplicateIconRequestsMap = filteredList.stream() + .filter(iconRequest -> { + // Filter out icons that should not share the same bitmap and title + if (iconRequest.itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) { + Log.e(TAG, + "Skipping Item info for deep shortcut: " + + iconRequest.itemInfo, + new IllegalStateException()); + return false; + } + return true; + }) .collect(groupingBy(iconRequest -> iconRequest.itemInfo.getTargetComponent())); Trace.beginSection("loadIconSubsectionInBulk"); - try (Cursor c = createBulkQueryCursor( - filteredList, - /* user = */ sectionKey.first, - /* useLowResIcons = */ sectionKey.second)) { - int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT); - while (c.moveToNext()) { - ComponentName cn = ComponentName.unflattenFromString( - c.getString(componentNameColumnIndex)); - List> duplicateIconRequests = - duplicateIconRequestsMap.get(cn); - - if (cn != null) { - CacheEntry entry = cacheLocked( - cn, - /* user = */ sectionKey.first, - () -> duplicateIconRequests.get(0).launcherActivityInfo, - mLauncherActivityInfoCachingLogic, - c, - /* usePackageIcon= */ false, - /* useLowResIcons = */ sectionKey.second); - - for (IconRequestInfo iconRequest : duplicateIconRequests) { - applyCacheEntry(entry, iconRequest.itemInfo); - } - } - } - } catch (SQLiteException e) { - Log.d(TAG, "Error reading icon cache", e); - } finally { - Trace.endSection(); - } + loadIconSubsection(sectionKey, filteredList, duplicateIconRequestsMap); + Trace.endSection(); }); Trace.endSection(); } + private void loadIconSubsection( + Pair sectionKey, + List> filteredList, + Map>> duplicateIconRequestsMap) { + Trace.beginSection("loadIconSubsectionWithDatabase"); + try (Cursor c = createBulkQueryCursor( + filteredList, + /* user = */ sectionKey.first, + /* useLowResIcons = */ sectionKey.second)) { + // Database title and icon loading + int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT); + while (c.moveToNext()) { + ComponentName cn = ComponentName.unflattenFromString( + c.getString(componentNameColumnIndex)); + List> duplicateIconRequests = + duplicateIconRequestsMap.get(cn); + + if (cn != null) { + CacheEntry entry = cacheLocked( + cn, + /* user = */ sectionKey.first, + () -> duplicateIconRequests.get(0).launcherActivityInfo, + mLauncherActivityInfoCachingLogic, + c, + /* usePackageIcon= */ false, + /* useLowResIcons = */ sectionKey.second); + + for (IconRequestInfo iconRequest : duplicateIconRequests) { + applyCacheEntry(entry, iconRequest.itemInfo); + } + } + } + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); + } finally { + Trace.endSection(); + } + + Trace.beginSection("loadIconSubsectionWithFallback"); + // Fallback title and icon loading + for (ComponentName cn : duplicateIconRequestsMap.keySet()) { + IconRequestInfo iconRequestInfo = duplicateIconRequestsMap.get(cn).get(0); + ItemInfoWithIcon itemInfo = iconRequestInfo.itemInfo; + BitmapInfo icon = itemInfo.bitmap; + boolean loadFallbackTitle = TextUtils.isEmpty(itemInfo.title); + boolean loadFallbackIcon = icon == null + || isDefaultIcon(icon, itemInfo.user) + || icon == BitmapInfo.LOW_RES_INFO; + + if (loadFallbackTitle || loadFallbackIcon) { + Log.i(TAG, + "Database bulk icon loading failed, using fallback bulk icon loading " + + "for: " + cn); + CacheEntry entry = new CacheEntry(); + LauncherActivityInfo lai = iconRequestInfo.launcherActivityInfo; + + // Fill fields that are not updated below so they are not subsequently + // deleted. + entry.title = itemInfo.title; + if (icon != null) { + entry.bitmap = icon; + } + entry.contentDescription = itemInfo.contentDescription; + + if (loadFallbackIcon) { + loadFallbackIcon( + lai, + entry, + mLauncherActivityInfoCachingLogic, + /* usePackageIcon= */ false, + /* usePackageTitle= */ loadFallbackTitle, + cn, + sectionKey.first); + } + if (loadFallbackTitle && TextUtils.isEmpty(entry.title) && lai != null) { + loadFallbackTitle( + lai, + entry, + mLauncherActivityInfoCachingLogic, + sectionKey.first); + } + + for (IconRequestInfo iconRequest : duplicateIconRequestsMap.get(cn)) { + applyCacheEntry(entry, iconRequest.itemInfo); + } + } + } + Trace.endSection(); + } /** * Fill in {@param infoInOut} with the corresponding icon and label. @@ -412,13 +482,39 @@ public class IconCache extends BaseIconCache { CacheEntry entry = getEntryForPackageLocked( infoInOut.packageName, infoInOut.user, useLowResIcon); applyCacheEntry(entry, infoInOut); - if (infoInOut.widgetCategory != NO_CATEGORY) { - WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext) - .get(infoInOut.widgetCategory); - infoInOut.title = mContext.getString(widgetSection.mSectionTitle); - infoInOut.contentDescription = mPackageManager.getUserBadgedLabel( - infoInOut.title, infoInOut.user); + if (infoInOut.widgetCategory == NO_CATEGORY) { + return; } + + WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext) + .get(infoInOut.widgetCategory); + infoInOut.title = mContext.getString(widgetSection.mSectionTitle); + infoInOut.contentDescription = mPackageManager.getUserBadgedLabel( + infoInOut.title, infoInOut.user); + final BitmapInfo cachedBitmap = mWidgetCategoryBitmapInfos.get(infoInOut.widgetCategory); + if (cachedBitmap != null) { + infoInOut.bitmap = getBadgedIcon(cachedBitmap, infoInOut.user); + return; + } + + try (LauncherIcons li = LauncherIcons.obtain(mContext)) { + final BitmapInfo tempBitmap = li.createBadgedIconBitmap( + mContext.getDrawable(widgetSection.mSectionDrawable), + new BaseIconFactory.IconOptions().setShrinkNonAdaptiveIcons(false)); + mWidgetCategoryBitmapInfos.put(infoInOut.widgetCategory, tempBitmap); + infoInOut.bitmap = getBadgedIcon(tempBitmap, infoInOut.user); + } catch (Exception e) { + Log.e(TAG, "Error initializing bitmap for icons with widget category", e); + } + + } + + private synchronized BitmapInfo getBadgedIcon(@Nullable final BitmapInfo bitmap, + @NonNull final UserHandle user) { + if (bitmap == null) { + return getDefaultIcon(user); + } + return bitmap.withFlags(getUserFlagOpLocked(user)); } protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) { diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java index e820ac474a..4b8c1ad590 100644 --- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java +++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java @@ -22,6 +22,7 @@ import android.os.UserHandle; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.icons.BaseIconFactory.IconOptions; import com.android.launcher3.icons.cache.CachingLogic; import com.android.launcher3.util.ResourceBasedOverride; @@ -59,7 +60,7 @@ public class LauncherActivityCachingLogic try (LauncherIcons li = LauncherIcons.obtain(context)) { return li.createBadgedIconBitmap(LauncherAppState.getInstance(context) .getIconProvider().getIcon(object, li.mFillResIconDpi), - object.getUser(), object.getApplicationInfo().targetSdkVersion); + new IconOptions().setUser(object.getUser())); } } } diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java new file mode 100644 index 0000000000..c4d5f2b5b5 --- /dev/null +++ b/src/com/android/launcher3/icons/LauncherIconProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.launcher3.R; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.Themes; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.Collections; +import java.util.Map; + +/** + * Extension of {@link IconProvider} with support for overriding theme icons + */ +public class LauncherIconProvider extends IconProvider { + + private static final String TAG_ICON = "icon"; + private static final String ATTR_PACKAGE = "package"; + private static final String ATTR_DRAWABLE = "drawable"; + + private static final String TAG = "LIconProvider"; + private static final Map DISABLED_MAP = Collections.emptyMap(); + + private Map mThemedIconMap; + private boolean mSupportsIconTheme; + + public LauncherIconProvider(Context context) { + super(context); + setIconThemeSupported(Themes.isThemedIconEnabled(context)); + } + + /** + * Enables or disables icon theme support + */ + public void setIconThemeSupported(boolean isSupported) { + mSupportsIconTheme = isSupported; + mThemedIconMap = isSupported && FeatureFlags.USE_LOCAL_ICON_OVERRIDES.get() + ? null : DISABLED_MAP; + } + + @Override + protected ThemeData getThemeDataForPackage(String packageName) { + return getThemedIconMap().get(packageName); + } + + @Override + public String getSystemIconState() { + return super.getSystemIconState() + (mSupportsIconTheme ? ",with-theme" : ",no-theme"); + } + + private Map getThemedIconMap() { + if (mThemedIconMap != null) { + return mThemedIconMap; + } + ArrayMap map = new ArrayMap<>(); + Resources res = mContext.getResources(); + try (XmlResourceParser parser = res.getXml(R.xml.grayscale_icon_map)) { + final int depth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT); + + while (((type = parser.next()) != XmlPullParser.END_TAG + || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + if (TAG_ICON.equals(parser.getName())) { + String pkg = parser.getAttributeValue(null, ATTR_PACKAGE); + int iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0); + if (iconId != 0 && !TextUtils.isEmpty(pkg)) { + map.put(pkg, new ThemeData(res, iconId)); + } + } + } + } catch (Exception e) { + Log.e(TAG, "Unable to parse icon map", e); + } + mThemedIconMap = map; + return mThemedIconMap; + } +} diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java index bf7897e8cc..5508c49410 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -21,6 +21,7 @@ import android.content.Context; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.graphics.IconShape; import com.android.launcher3.graphics.LauncherPreviewRenderer; +import com.android.launcher3.util.Themes; /** * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class @@ -32,18 +33,13 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { private static LauncherIcons sPool; private static int sPoolId = 0; - public static LauncherIcons obtain(Context context) { - return obtain(context, IconShape.getShape().enableShapeDetection()); - } - /** * Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases. */ - public static LauncherIcons obtain(Context context, boolean shapeDetection) { + public static LauncherIcons obtain(Context context) { if (context instanceof LauncherPreviewRenderer.PreviewContext) { - return ((LauncherPreviewRenderer.PreviewContext) context).newLauncherIcons(context, - shapeDetection); + return ((LauncherPreviewRenderer.PreviewContext) context).newLauncherIcons(context); } int poolId; @@ -58,8 +54,7 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { } InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); - return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId, - shapeDetection); + return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId); } public static void clearPool() { @@ -73,9 +68,9 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { private LauncherIcons next; - protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId, - boolean shapeDetection) { - super(context, fillResIconDpi, iconBitmapSize, shapeDetection); + protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) { + super(context, fillResIconDpi, iconBitmapSize, IconShape.getShape().enableShapeDetection()); + mMonoIconEnabled = Themes.isThemedIconEnabled(context); mPoolId = poolId; } diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java index d7eed06901..6a8f34a93d 100644 --- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java +++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java @@ -71,8 +71,8 @@ public class ShortcutCachingLogic implements CachingLogic { Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon( context, info, LauncherAppState.getIDP(context).fillResIconDpi); if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO; - return new BitmapInfo(li.createScaledBitmapWithoutShadow( - unbadgedDrawable, 0), Themes.getColorAccent(context)); + return new BitmapInfo(li.createScaledBitmapWithoutShadow(unbadgedDrawable), + Themes.getColorAccent(context)); } } diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index 8b7bebc621..c4ec4e36fd 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -258,6 +258,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User swipes or fling in DOWN direction to close apps drawer.") LAUNCHER_ALLAPPS_CLOSE_DOWN(569), + @UiEvent(doc = "User tap outside apps drawer sheet to close apps drawer.") + LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE(941), + @UiEvent(doc = "User swipes or fling in UP direction and hold from the bottom bazel area") LAUNCHER_OVERVIEW_GESTURE(570), @@ -372,6 +375,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "Notification dismissed by swiping right.") LAUNCHER_NOTIFICATION_DISMISSED(652), + @UiEvent(doc = "Current grid size is changed to 6.") + LAUNCHER_GRID_SIZE_6(930), + @UiEvent(doc = "Current grid size is changed to 5.") LAUNCHER_GRID_SIZE_5(662), @@ -521,7 +527,72 @@ public class StatsLogManager implements ResourceBasedOverride { LAUNCHER_TASKBAR_LONGPRESS_SHOW(897), @UiEvent(doc = "User clicks on the search icon on header to launch search in app.") - LAUNCHER_ALLAPPS_SEARCHINAPP_LAUNCH(913); + LAUNCHER_ALLAPPS_SEARCHINAPP_LAUNCH(913), + + @UiEvent(doc = "User is shown the back gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_BACK_STEP_SHOWN(959), + + @UiEvent(doc = "User is shown the home gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_HOME_STEP_SHOWN(960), + + @UiEvent(doc = "User is shown the overview gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_OVERVIEW_STEP_SHOWN(961), + + @UiEvent(doc = "User completed the back gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_BACK_STEP_COMPLETED(962), + + @UiEvent(doc = "User completed the home gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_HOME_STEP_COMPLETED(963), + + @UiEvent(doc = "User completed the overview gesture navigation tutorial step.") + LAUNCHER_GESTURE_TUTORIAL_OVERVIEW_STEP_COMPLETED(964), + + @UiEvent(doc = "User skips the gesture navigation tutorial.") + LAUNCHER_GESTURE_TUTORIAL_SKIPPED(965), + + @UiEvent(doc = "User scrolled on one of the all apps surfaces such as A-Z list, search " + + "result page etc.") + LAUNCHER_ALLAPPS_SCROLLED(985), + + @UiEvent(doc = "User tapped taskbar home button") + LAUNCHER_TASKBAR_HOME_BUTTON_TAP(1003), + + @UiEvent(doc = "User tapped taskbar back button") + LAUNCHER_TASKBAR_BACK_BUTTON_TAP(1004), + + @UiEvent(doc = "User tapped taskbar overview/recents button") + LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP(1005), + + @UiEvent(doc = "User tapped taskbar IME switcher button") + LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP(1006), + + @UiEvent(doc = "User tapped taskbar a11y button") + LAUNCHER_TASKBAR_A11Y_BUTTON_TAP(1007), + + @UiEvent(doc = "User tapped taskbar home button") + LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS(1008), + + @UiEvent(doc = "User tapped taskbar back button") + LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS(1009), + + @UiEvent(doc = "User tapped taskbar overview/recents button") + LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS(1010), + + @UiEvent(doc = "User tapped taskbar a11y button") + LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS(1011), + + @UiEvent(doc = "Show an 'Undo' snackbar when users dismiss a predicted hotseat item") + LAUNCHER_DISMISS_PREDICTION_UNDO(1035), + + @UiEvent(doc = "User clicked on IME quicksearch button.") + LAUNCHER_ALLAPPS_QUICK_SEARCH_WITH_IME(1047), + + @UiEvent(doc = "User tapped taskbar All Apps button.") + LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP(1057), + + @UiEvent(doc = "User tapped on Share app system shortcut.") + LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP(1075), + ; // ADD MORE @@ -556,7 +627,7 @@ public class StatsLogManager implements ResourceBasedOverride { } /** - * Helps to construct and write the log message. + * Helps to construct and log launcher event. */ public interface StatsLogger { @@ -655,6 +726,74 @@ public class StatsLogManager implements ResourceBasedOverride { } } + /** + * Helps to construct and log latency event. + */ + public interface StatsLatencyLogger { + + enum LatencyType { + UNKNOWN(0), + COLD(1), + HOT(2), + TIMEOUT(3), + FAIL(4), + COLD_USERWAITING(5); + + private final int mId; + + LatencyType(int id) { + this.mId = id; + } + + public int getId() { + return mId; + } + + } + + /** + * Sets {@link InstanceId} of log message. + */ + default StatsLatencyLogger withInstanceId(InstanceId instanceId) { + return this; + } + + + /** + * Sets latency of the event. + */ + default StatsLatencyLogger withLatency(long latencyInMillis) { + return this; + } + + /** + * Sets {@link LatencyType} of log message. + */ + default StatsLatencyLogger withType(LatencyType type) { + return this; + } + + /** + * Sets query length of the event. + */ + default StatsLatencyLogger withQueryLength(int queryLength) { + return this; + } + + /** + * Sets packageId of log message. + */ + default StatsLatencyLogger withPackageId(int packageId) { + return this; + } + + /** + * Builds the final message and logs it as {@link EventEnum}. + */ + default void log(EventEnum event) { + } + } + /** * Returns new logger object. */ @@ -666,11 +805,27 @@ public class StatsLogManager implements ResourceBasedOverride { return logger; } + /** + * Returns new latency logger object. + */ + public StatsLatencyLogger latencyLogger() { + StatsLatencyLogger logger = createLatencyLogger(); + if (mInstanceId != null) { + logger.withInstanceId(mInstanceId); + } + return logger; + } + protected StatsLogger createLogger() { return new StatsLogger() { }; } + protected StatsLatencyLogger createLatencyLogger() { + return new StatsLatencyLogger() { + }; + } + /** * Sets InstanceId to every new {@link StatsLogger} object returned by {@link #logger()} when * not-null. diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java index a13fa55dff..31ef2e5ea6 100644 --- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java +++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java @@ -15,33 +15,29 @@ */ package com.android.launcher3.model; -import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; - import android.content.Intent; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller.SessionInfo; import android.os.UserHandle; -import android.util.LongSparseArray; +import android.util.Log; import android.util.Pair; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel.CallbackTask; import com.android.launcher3.LauncherSettings; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.PackageInstallInfo; -import com.android.launcher3.util.GridOccupancy; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.IntArray; -import com.android.launcher3.util.IntSet; import com.android.launcher3.util.PackageManagerHelper; import java.util.ArrayList; @@ -56,11 +52,23 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { private final List> mItemList; + private final WorkspaceItemSpaceFinder mItemSpaceFinder; + /** * @param itemList items to add on the workspace */ public AddWorkspaceItemsTask(List> itemList) { + this(itemList, new WorkspaceItemSpaceFinder()); + } + + /** + * @param itemList items to add on the workspace + * @param itemSpaceFinder inject WorkspaceItemSpaceFinder dependency for testing + */ + public AddWorkspaceItemsTask(List> itemList, + WorkspaceItemSpaceFinder itemSpaceFinder) { mItemList = itemList; + mItemSpaceFinder = itemSpaceFinder; } @Override @@ -72,7 +80,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { final ArrayList addedItemsFinal = new ArrayList<>(); final IntArray addedWorkspaceScreensFinal = new IntArray(); - synchronized(dataModel) { + synchronized (dataModel) { IntArray workspaceScreens = dataModel.collectWorkspaceScreens(); List filteredItems = new ArrayList<>(); @@ -82,18 +90,26 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { // Short-circuit this logic if the icon exists somewhere on the workspace if (shortcutExists(dataModel, item.getIntent(), item.user)) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + LOG + " Item already on workspace."); + } continue; } // b/139663018 Short-circuit this logic if the icon is a system app if (PackageManagerHelper.isSystemApp(app.getContext(), item.getIntent())) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + LOG + " Item is a system app."); + } continue; } } if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - if (item instanceof AppInfo) { - item = ((AppInfo) item).makeWorkspaceItem(); + if (item instanceof WorkspaceItemFactory) { + item = ((WorkspaceItemFactory) item).makeWorkspaceItem(app.getContext()); } } if (item != null) { @@ -107,7 +123,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { for (ItemInfo item : filteredItems) { // Find appropriate space for the item. - int[] coords = findSpaceForItem(app, dataModel, workspaceScreens, + int[] coords = mItemSpaceFinder.findSpaceForItem(app, dataModel, workspaceScreens, addedWorkspaceScreensFinal, item.spanX, item.spanY); int screenId = coords[0]; @@ -115,8 +131,8 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { if (item instanceof WorkspaceItemInfo || item instanceof FolderInfo || item instanceof LauncherAppWidgetInfo) { itemInfo = item; - } else if (item instanceof AppInfo) { - itemInfo = ((AppInfo) item).makeWorkspaceItem(); + } else if (item instanceof WorkspaceItemFactory) { + itemInfo = ((WorkspaceItemFactory) item).makeWorkspaceItem(app.getContext()); } else { throw new RuntimeException("Unexpected info type"); } @@ -126,6 +142,9 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { String packageName = item.getTargetComponent() != null ? item.getTargetComponent().getPackageName() : null; if (packageName == null) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " Null packageName."); + } continue; } SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user, @@ -134,6 +153,9 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { if (!packageInstaller.verifySessionInfo(sessionInfo)) { FileLog.d(LOG, "Item info failed session info verification. " + "Skipping : " + workspaceInfo); + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + "Failed verification."); + } continue; } @@ -144,6 +166,9 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { if (sessionInfo == null) { if (!hasActivity) { // Session was cancelled, do not add. + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + "Session cancelled"); + } continue; } } else { @@ -156,13 +181,16 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { // App was installed while launcher was in the background, // or app was already installed for another user. itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user) - .makeWorkspaceItem(); + .makeWorkspaceItem(app.getContext()); if (shortcutExists(dataModel, itemInfo.getIntent(), itemInfo.user)) { // We need this additional check here since we treat all auto added // workspace items as promise icons. At this point we now have the // correct intent to compare against existing workspace icons. // Icon already exists on the workspace and should not be auto-added. + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + "shortcutExists"); + } continue; } @@ -266,82 +294,4 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { } return false; } - - /** - * Find a position on the screen for the given size or adds a new screen. - * @return screenId and the coordinates for the item in an int array of size 3. - */ - protected int[] findSpaceForItem( LauncherAppState app, BgDataModel dataModel, - IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) { - LongSparseArray> screenItems = new LongSparseArray<>(); - - // Use sBgItemsIdMap as all the items are already loaded. - synchronized (dataModel) { - for (ItemInfo info : dataModel.itemsIdMap) { - if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { - ArrayList items = screenItems.get(info.screenId); - if (items == null) { - items = new ArrayList<>(); - screenItems.put(info.screenId, items); - } - items.add(info); - } - } - } - - // Find appropriate space for the item. - int screenId = 0; - int[] coordinates = new int[2]; - boolean found = false; - - int screenCount = workspaceScreens.size(); - // First check the preferred screen. - IntSet screensToExclude = new IntSet(); - if (FeatureFlags.QSB_ON_FIRST_SCREEN) { - screensToExclude.add(FIRST_SCREEN_ID); - } - - for (int screen = 0; screen < screenCount; screen++) { - screenId = workspaceScreens.get(screen); - if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen( - app, screenItems.get(screenId), coordinates, spanX, spanY)) { - // We found a space for it - found = true; - break; - } - } - - if (!found) { - // Still no position found. Add a new screen to the end. - screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(), - LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) - .getInt(LauncherSettings.Settings.EXTRA_VALUE); - - // Save the screen id for binding in the workspace - workspaceScreens.add(screenId); - addedWorkspaceScreensFinal.add(screenId); - - // If we still can't find an empty space, then God help us all!!! - if (!findNextAvailableIconSpaceInScreen( - app, screenItems.get(screenId), coordinates, spanX, spanY)) { - throw new RuntimeException("Can't find space to add the item"); - } - } - return new int[] {screenId, coordinates[0], coordinates[1]}; - } - - private boolean findNextAvailableIconSpaceInScreen( - LauncherAppState app, ArrayList occupiedPos, - int[] xy, int spanX, int spanY) { - InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); - - GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows); - if (occupiedPos != null) { - for (ItemInfo r : occupiedPos) { - occupied.markCells(r, true); - } - } - return occupied.findVacantCell(xy, spanX, spanY); - } - } diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java index dbed9a9d58..4875d83942 100644 --- a/src/com/android/launcher3/model/AllAppsList.java +++ b/src/com/android/launcher3/model/AllAppsList.java @@ -36,9 +36,9 @@ import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.util.FlagOp; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.SafeCloseable; @@ -47,6 +47,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.function.Consumer; +import java.util.function.Predicate; /** @@ -143,6 +144,8 @@ public class AllAppsList { if (loadIcon) { mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */); info.sectionName = mIndex.computeSectionName(info.title); + } else { + info.title = ""; } data.add(info); @@ -167,6 +170,8 @@ public class AllAppsList { if (loadIcon) { mIconCache.getTitleAndIcon(promiseAppInfo, promiseAppInfo.usingLowResIcon()); promiseAppInfo.sectionName = mIndex.computeSectionName(promiseAppInfo.title); + } else { + promiseAppInfo.title = ""; } data.add(promiseAppInfo); @@ -253,11 +258,11 @@ public class AllAppsList { /** * Updates the disabled flags of apps matching {@param matcher} based on {@param op}. */ - public void updateDisabledFlags(ItemInfoMatcher matcher, FlagOp op) { + public void updateDisabledFlags(Predicate matcher, FlagOp op) { final List data = this.data; for (int i = data.size() - 1; i >= 0; i--) { AppInfo info = data.get(i); - if (matcher.matches(info, info.componentName)) { + if (matcher.test(info)) { info.runtimeStatusFlags = op.apply(info.runtimeStatusFlags); mDataChanged = true; } diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java index d270cc56a1..b50ab587f4 100644 --- a/src/com/android/launcher3/model/BaseLoaderResults.java +++ b/src/com/android/launcher3/model/BaseLoaderResults.java @@ -18,7 +18,6 @@ package com.android.launcher3.model; import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; -import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.os.Process; @@ -27,6 +26,8 @@ import android.util.Log; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel.CallbackTask; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; @@ -42,6 +43,7 @@ import com.android.launcher3.util.RunnableList; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -110,6 +112,42 @@ public abstract class BaseLoaderResults { public abstract void bindWidgets(); + /** + * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right) + */ + protected void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile, + ArrayList workspaceItems) { + final int screenCols = profile.numColumns; + final int screenCellCount = profile.numColumns * profile.numRows; + Collections.sort(workspaceItems, (lhs, rhs) -> { + if (lhs.container == rhs.container) { + // Within containers, order by their spatial position in that container + switch (lhs.container) { + case LauncherSettings.Favorites.CONTAINER_DESKTOP: { + int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols + + lhs.cellX); + int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols + + rhs.cellX); + return Integer.compare(lr, rr); + } + case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { + // We currently use the screen id as the rank + return Integer.compare(lhs.screenId, rhs.screenId); + } + default: + if (FeatureFlags.IS_STUDIO_BUILD) { + throw new RuntimeException( + "Unexpected container type when sorting workspace items."); + } + return 0; + } + } else { + // Between containers, order by hotseat, desktop + return Integer.compare(lhs.container, rhs.container); + } + }); + } + protected void executeCallbacksTask(CallbackTask task, Executor executor) { executor.execute(() -> { if (mMyBindingId != mBgDataModel.lastBindId) { @@ -131,7 +169,7 @@ public abstract class BaseLoaderResults { return idleLock; } - private static class WorkspaceBinder { + private class WorkspaceBinder { private final Executor mUiExecutor; private final Callbacks mCallbacks; @@ -166,7 +204,9 @@ public abstract class BaseLoaderResults { } private void bind() { - IntSet currentScreenIds = mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); + final IntSet currentScreenIds = + mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); + Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks); // Separate the items that are on the current screen, and all the other remaining items ArrayList currentWorkspaceItems = new ArrayList<>(); @@ -226,6 +266,8 @@ public abstract class BaseLoaderResults { MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); c.onInitialBindComplete(currentScreenIds, pendingTasks); }, mUiExecutor); + + mCallbacks.bindStringCache(mBgDataModel.stringCache.clone()); } private void bindWorkspaceItems( diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java index a3a471775c..2a6a6919e0 100644 --- a/src/com/android/launcher3/model/BaseModelUpdateTask.java +++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java @@ -17,6 +17,8 @@ package com.android.launcher3.model; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherModel.CallbackTask; @@ -27,7 +29,6 @@ import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.util.ArrayList; @@ -35,6 +36,7 @@ import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -128,8 +130,9 @@ public abstract class BaseModelUpdateTask implements ModelUpdateTask { scheduleCallbackTask(c -> c.bindAllWidgets(widgets)); } - public void deleteAndBindComponentsRemoved(final ItemInfoMatcher matcher) { - getModelWriter().deleteItemsFromDatabase(matcher); + public void deleteAndBindComponentsRemoved(final Predicate matcher, + @Nullable final String reason) { + getModelWriter().deleteItemsFromDatabase(matcher, reason); // Call the components-removed callback scheduleCallbackTask(c -> c.bindWorkspaceComponentsRemoved(matcher)); diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index d3351dcc59..de23c4b31f 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -31,6 +31,7 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.LauncherSettings; @@ -50,7 +51,6 @@ import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.IntSparseArrayMap; -import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.RunnableList; import com.android.launcher3.widget.model.WidgetsListBaseEntry; @@ -66,6 +66,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -113,6 +114,11 @@ public class BgDataModel { */ public final WidgetsModel widgetsModel = new WidgetsModel(); + /** + * Cache for strings used in launcher + */ + public final StringCache stringCache = new StringCache(); + /** * Id when the model was last bound */ @@ -229,7 +235,6 @@ public class BgDataModel { String.format("Adding item to ID map: %s", item.toString()), /* stackTrace= */ null); } - itemsIdMap.put(item.id, item); switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: @@ -465,6 +470,7 @@ public class BgDataModel { * or an empty IntSet * @param orderedScreenIds All the page ids to be bound */ + @NonNull default IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) { return new IntSet(); } @@ -491,7 +497,7 @@ public class BgDataModel { default void bindWorkspaceItemsChanged(List updated) { } default void bindWidgetsRestored(ArrayList widgets) { } default void bindRestoreItemsChange(HashSet updates) { } - default void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { } + default void bindWorkspaceComponentsRemoved(Predicate matcher) { } default void bindAllWidgets(List widgets) { } default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) { @@ -506,5 +512,10 @@ public class BgDataModel { default void bindExtraContainerItems(FixedContainerItems item) { } default void bindAllApplications(AppInfo[] apps, int flags) { } + + /** + * Binds the cache of string resources + */ + default void bindStringCache(StringCache cache) { } } } diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java index fa11d4e395..35fcb789e7 100644 --- a/src/com/android/launcher3/model/DeviceGridState.java +++ b/src/com/android/launcher3/model/DeviceGridState.java @@ -22,6 +22,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_3; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_5; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_6; import android.content.Context; import android.content.SharedPreferences; @@ -37,20 +38,23 @@ import java.util.Objects; /** * Utility class representing persisted grid properties. */ -public class DeviceGridState { +public class DeviceGridState implements Comparable { public static final String KEY_WORKSPACE_SIZE = "migration_src_workspace_size"; public static final String KEY_HOTSEAT_COUNT = "migration_src_hotseat_count"; public static final String KEY_DEVICE_TYPE = "migration_src_device_type"; + public static final String KEY_DB_FILE = "migration_src_db_file"; private final String mGridSizeString; private final int mNumHotseat; private final @DeviceType int mDeviceType; + private final String mDbFile; public DeviceGridState(InvariantDeviceProfile idp) { mGridSizeString = String.format(Locale.ENGLISH, "%d,%d", idp.numColumns, idp.numRows); mNumHotseat = idp.numDatabaseHotseatIcons; mDeviceType = idp.deviceType; + mDbFile = idp.dbFile; } public DeviceGridState(Context context) { @@ -58,6 +62,7 @@ public class DeviceGridState { mGridSizeString = prefs.getString(KEY_WORKSPACE_SIZE, ""); mNumHotseat = prefs.getInt(KEY_HOTSEAT_COUNT, -1); mDeviceType = prefs.getInt(KEY_DEVICE_TYPE, TYPE_PHONE); + mDbFile = prefs.getString(KEY_DB_FILE, ""); } /** @@ -67,6 +72,20 @@ public class DeviceGridState { return mDeviceType; } + /** + * Returns the databaseFile for the grid. + */ + public String getDbFile() { + return mDbFile; + } + + /** + * Returns the number of hotseat icons. + */ + public int getNumHotseat() { + return mNumHotseat; + } + /** * Stores the device state to shared preferences */ @@ -75,6 +94,7 @@ public class DeviceGridState { .putString(KEY_WORKSPACE_SIZE, mGridSizeString) .putInt(KEY_HOTSEAT_COUNT, mNumHotseat) .putInt(KEY_DEVICE_TYPE, mDeviceType) + .putString(KEY_DB_FILE, mDbFile) .apply(); } @@ -83,14 +103,16 @@ public class DeviceGridState { */ public LauncherEvent getWorkspaceSizeEvent() { if (!TextUtils.isEmpty(mGridSizeString)) { - switch (mGridSizeString.charAt(0)) { - case '5': + switch (getColumns()) { + case 6: + return LAUNCHER_GRID_SIZE_6; + case 5: return LAUNCHER_GRID_SIZE_5; - case '4': + case 4: return LAUNCHER_GRID_SIZE_4; - case '3': + case 3: return LAUNCHER_GRID_SIZE_3; - case '2': + case 2: return LAUNCHER_GRID_SIZE_2; } } @@ -103,6 +125,7 @@ public class DeviceGridState { + "mGridSizeString='" + mGridSizeString + '\'' + ", mNumHotseat=" + mNumHotseat + ", mDeviceType=" + mDeviceType + + ", mDbFile=" + mDbFile + '}'; } @@ -116,4 +139,21 @@ public class DeviceGridState { return mNumHotseat == other.mNumHotseat && Objects.equals(mGridSizeString, other.mGridSizeString); } + + public Integer getColumns() { + return Integer.parseInt(String.valueOf(mGridSizeString.charAt(0))); + } + + public Integer getRows() { + return Integer.parseInt(String.valueOf(mGridSizeString.charAt(2))); + } + + @Override + public int compareTo(DeviceGridState other) { + Integer size = getColumns() * getRows(); + Integer otherSize = other.getColumns() * other.getRows(); + + return size.compareTo(otherSize); + } + } diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java index e391d37938..9e91b9d6e8 100644 --- a/src/com/android/launcher3/model/FirstScreenBroadcast.java +++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java @@ -20,6 +20,7 @@ import static android.app.PendingIntent.FLAG_ONE_SHOT; import static android.os.Process.myUserHandle; import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; @@ -31,13 +32,18 @@ import android.content.pm.PackageInstaller.SessionInfo; import android.os.UserHandle; import android.util.Log; +import androidx.annotation.AnyThread; +import androidx.annotation.WorkerThread; + import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.PackageUserKey; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -78,6 +84,7 @@ public class FirstScreenBroadcast { * Sends a broadcast to all package installers that have items with active sessions on the users * first screen. */ + @WorkerThread public void sendBroadcasts(Context context, List firstScreenItems) { UserHandle myUser = myUserHandle(); mSessionInfoForPackage @@ -95,6 +102,7 @@ public class FirstScreenBroadcast { * @param packages List of packages with active sessions for this package installer. * @param firstScreenItems List of items on the first screen. */ + @WorkerThread private void sendBroadcastToInstaller(Context context, String installerPackageName, Set packages, List firstScreenItems) { Set folderItems = new HashSet<>(); @@ -106,7 +114,7 @@ public class FirstScreenBroadcast { if (info instanceof FolderInfo) { FolderInfo folderInfo = (FolderInfo) info; String folderItemInfoPackage; - for (ItemInfo folderItemInfo : folderInfo.contents) { + for (ItemInfo folderItemInfo : cloneOnMainThread(folderInfo.contents)) { folderItemInfoPackage = getPackageName(folderItemInfo); if (folderItemInfoPackage != null && packages.contains(folderItemInfoPackage)) { @@ -135,6 +143,13 @@ public class FirstScreenBroadcast { printList(installerPackageName, "Widget item", widgetItems); } + if (folderItems.isEmpty() + && workspaceItems.isEmpty() + && hotseatItems.isEmpty() + && widgetItems.isEmpty()) { + // Avoid sending broadcast if there is nothing to send. + return; + } context.sendBroadcast(new Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS) .setPackage(installerPackageName) .putStringArrayListExtra(FOLDER_ITEM_EXTRA, new ArrayList<>(folderItems)) @@ -163,4 +178,17 @@ public class FirstScreenBroadcast { Log.d(TAG, packageInstaller + ":" + label + ":" + pkg); } } + + /** + * Clone the provided list on UI thread. This is used for {@link FolderInfo#contents} which + * is always modified on UI thread. + */ + @AnyThread + private static List cloneOnMainThread(ArrayList list) { + try { + return MAIN_EXECUTOR.submit(() -> new ArrayList(list)).get(); + } catch (Exception e) { + return Collections.emptyList(); + } + } } diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java index ca680b72c6..ef9250c900 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java @@ -22,7 +22,6 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; @@ -38,6 +37,7 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.LauncherPreviewRenderer; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.InstallSessionHelper; @@ -103,13 +103,16 @@ public class GridSizeMigrationTaskV2 { * Check given a new IDP, if migration is necessary. */ public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) { - DeviceGridState idpGridState = new DeviceGridState(idp); - DeviceGridState contextGridState = new DeviceGridState(context); - boolean needsToMigrate = !idpGridState.isCompatible(contextGridState); + return needsToMigrate(new DeviceGridState(context), new DeviceGridState(idp)); + } + + private static boolean needsToMigrate( + DeviceGridState srcDeviceState, DeviceGridState destDeviceState) { + boolean needsToMigrate = !destDeviceState.isCompatible(srcDeviceState); // TODO(b/198965093): Revert this change after bug is fixed if (needsToMigrate) { - Log.d("b/198965093", "Migration is needed. idpGridState: " + idpGridState - + ", contextGridState: " + contextGridState); + Log.d("b/198965093", "Migration is needed. destDeviceState: " + destDeviceState + + ", srcDeviceState: " + srcDeviceState); } return needsToMigrate; } @@ -142,23 +145,26 @@ public class GridSizeMigrationTaskV2 { idp = LauncherAppState.getIDP(context); } - if (!needsToMigrate(context, idp)) { + DeviceGridState srcDeviceState = new DeviceGridState(context); + DeviceGridState destDeviceState = new DeviceGridState(idp); + if (!needsToMigrate(srcDeviceState, destDeviceState)) { return true; } - SharedPreferences prefs = Utilities.getPrefs(context); HashSet validPackages = getValidPackages(context); if (migrateForPreview) { if (!LauncherSettings.Settings.call( context.getContentResolver(), - LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW, idp.dbFile).getBoolean( + LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW, + destDeviceState.getDbFile()).getBoolean( LauncherSettings.Settings.EXTRA_VALUE)) { return false; } } else if (!LauncherSettings.Settings.call( context.getContentResolver(), - LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER).getBoolean( + LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER, + destDeviceState.getDbFile()).getBoolean( LauncherSettings.Settings.EXTRA_VALUE)) { return false; } @@ -178,10 +184,10 @@ public class GridSizeMigrationTaskV2 { : LauncherSettings.Favorites.TABLE_NAME, context, validPackages); - Point targetSize = new Point(idp.numColumns, idp.numRows); + Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows()); GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(), - srcReader, destReader, idp.numDatabaseHotseatIcons, targetSize); - task.migrate(idp); + srcReader, destReader, destDeviceState.getNumHotseat(), targetSize); + task.migrate(srcDeviceState, destDeviceState); if (!migrateForPreview) { dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE); @@ -199,25 +205,26 @@ public class GridSizeMigrationTaskV2 { if (!migrateForPreview) { // Save current configuration, so that the migration does not run again. - new DeviceGridState(idp).writeToPrefs(context); + destDeviceState.writeToPrefs(context); } } } @VisibleForTesting - protected boolean migrate(InvariantDeviceProfile idp) { + protected boolean migrate(DeviceGridState srcDeviceState, DeviceGridState destDeviceState) { if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) { return false; } + // Sort the items by the reading order. + Collections.sort(mHotseatDiff); + Collections.sort(mWorkspaceDiff); + // Migrate hotseat HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader, mDestReader, mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff); hotseatSolution.find(); - // Sort the items by the reading order. - Collections.sort(mWorkspaceDiff); - // Migrate workspace. // First we create a collection of the screens List screens = new ArrayList<>(); @@ -225,13 +232,19 @@ public class GridSizeMigrationTaskV2 { screens.add(screenId); } + boolean preservePages = false; + if (screens.isEmpty() && FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.get()) { + preservePages = destDeviceState.compareTo(srcDeviceState) >= 0 + && destDeviceState.getColumns() - srcDeviceState.getColumns() <= 2; + } + // Then we place the items on the screens for (int screenId : screens) { if (DEBUG) { Log.d(TAG, "Migrating " + screenId); } GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader, - mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff); + mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff, false); workspaceSolution.find(); if (mWorkspaceDiff.isEmpty()) { break; @@ -243,10 +256,12 @@ public class GridSizeMigrationTaskV2 { int screenId = mDestReader.mLastScreenId + 1; while (!mWorkspaceDiff.isEmpty()) { GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader, - mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff); + mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff, + preservePages); workspaceSolution.find(); screenId++; } + return true; } @@ -363,13 +378,15 @@ public class GridSizeMigrationTaskV2 { private final int mScreenId; private final int mTrgX; private final int mTrgY; - private final List mItemsToPlace; + private final List mSortedItemsToPlace; + private final boolean mMatchingScreenIdOnly; private int mNextStartX; private int mNextStartY; GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader, - Context context, int screenId, int trgX, int trgY, List itemsToPlace) { + Context context, int screenId, int trgX, int trgY, List sortedItemsToPlace, + boolean matchingScreenIdOnly) { mDb = db; mSrcReader = srcReader; mDestReader = destReader; @@ -379,20 +396,24 @@ public class GridSizeMigrationTaskV2 { mTrgX = trgX; mTrgY = trgY; mNextStartX = 0; - mNextStartY = mTrgY - 1; + mNextStartY = mScreenId == 0 && FeatureFlags.QSB_ON_FIRST_SCREEN + ? 1 /* smartspace */ : 0; List existedEntries = mDestReader.mWorkspaceEntriesByScreenId.get(screenId); if (existedEntries != null) { for (DbEntry entry : existedEntries) { mOccupied.markCells(entry, true); } } - mItemsToPlace = itemsToPlace; + mSortedItemsToPlace = sortedItemsToPlace; + mMatchingScreenIdOnly = matchingScreenIdOnly; } public void find() { - Iterator iterator = mItemsToPlace.iterator(); + Iterator iterator = mSortedItemsToPlace.iterator(); while (iterator.hasNext()) { final DbEntry entry = iterator.next(); + if (mMatchingScreenIdOnly && entry.screenId < mScreenId) continue; + if (mMatchingScreenIdOnly && entry.screenId > mScreenId) break; if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { iterator.remove(); continue; @@ -411,7 +432,7 @@ public class GridSizeMigrationTaskV2 { * to speed up the search. */ private boolean findPlacement(DbEntry entry) { - for (int y = mNextStartY; y >= (mScreenId == 0 ? 1 /* smartspace */ : 0); y--) { + for (int y = mNextStartY; y < mTrgY; y++) { for (int x = mNextStartX; x < mTrgX; x++) { boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY); boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX, @@ -494,7 +515,7 @@ public class GridSizeMigrationTaskV2 { private final SQLiteDatabase mDb; private final String mTableName; private final Context mContext; - private final HashSet mValidPackages; + private final Set mValidPackages; private int mLastScreenId = -1; private final ArrayList mHotseatEntries = new ArrayList<>(); @@ -503,7 +524,7 @@ public class GridSizeMigrationTaskV2 { new ArrayMap<>(); DbReader(SQLiteDatabase db, String tableName, Context context, - HashSet validPackages) { + Set validPackages) { mDb = db; mTableName = tableName; mContext = context; @@ -734,7 +755,7 @@ public class GridSizeMigrationTaskV2 { return Integer.compare(screenId, another.screenId); } if (cellY != another.cellY) { - return -Integer.compare(cellY, another.cellY); + return Integer.compare(cellY, another.cellY); } return Integer.compare(cellX, another.cellX); } diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java index 217f523e14..5a220f74c7 100644 --- a/src/com/android/launcher3/model/ItemInstallQueue.java +++ b/src/com/android/launcher3/model/ItemInstallQueue.java @@ -49,6 +49,7 @@ import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.PersistedItemArray; import com.android.launcher3.util.Preconditions; @@ -118,10 +119,18 @@ public class ItemInstallQueue { Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity(); if (launcher == null) { // Launcher not loaded + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + LOG + " flushQueueInBackground launcher not loaded"); + } return; } ensureQueueLoaded(); if (mItems.isEmpty()) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + LOG + " flushQueueInBackground no items to load"); + } return; } @@ -131,6 +140,10 @@ public class ItemInstallQueue { // Add the items and clear queue if (!installQueue.isEmpty()) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, + LOG + " flushQueueInBackground launcher addAndBindAddedWorkspaceItems"); + } // add log launcher.getModel().addAndBindAddedWorkspaceItems(installQueue); } @@ -191,6 +204,10 @@ public class ItemInstallQueue { // Queue the item up for adding if launcher has not loaded properly yet MODEL_EXECUTOR.post(() -> { Pair itemInfo = info.getItemInfo(mContext); + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " queuePendingShortcutInfo" + + ", itemInfo=" + itemInfo); + } if (itemInfo == null) { FileLog.d(LOG, "Adding PendingInstallShortcutInfo with no attached info to queue.", diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 178fbdb7b8..ae5b66ad88 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -456,11 +456,13 @@ public class LoaderCursor extends CursorWrapper { if (!occupied.containsKey(item.screenId)) { GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1); - if (item.screenId == Workspace.FIRST_SCREEN_ID) { - // Mark the first row as occupied (if the feature is enabled) - // in order to account for the QSB. + if (item.screenId == Workspace.FIRST_SCREEN_ID && FeatureFlags.QSB_ON_FIRST_SCREEN) { + // Mark the first X columns (X is width of the search container) in the first row as + // occupied (if the feature is enabled) in order to account for the search + // container. + int spanX = mIDP.numSearchContainerColumns; int spanY = FeatureFlags.EXPANDED_SMARTSPACE.get() ? 2 : 1; - screen.markCells(0, 0, countX + 1, spanY, FeatureFlags.QSB_ON_FIRST_SCREEN); + screen.markCells(0, 0, spanX, spanY, true); } occupied.put(item.screenId, screen); } diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 2a0f9a6f67..f1c5d59fc8 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -615,7 +615,13 @@ public class LoaderTask implements Runnable { } if (info != null) { - iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon)); + if (info.itemType + != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + // Skip deep shortcuts; their title and icons have already been + // loaded above. + iconRequestInfos.add( + c.createIconRequestInfo(info, useLowResIcon)); + } c.applyCommonProperties(info); @@ -857,6 +863,9 @@ public class LoaderTask implements Runnable { // Load delegate items mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts); + // Load string cache + mModelDelegate.loadStringCache(mBgDataModel.stringCache); + // Break early if we've stopped loading if (mStopped) { mBgDataModel.clear(); diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java index 765141a0e2..3bd9470566 100644 --- a/src/com/android/launcher3/model/ModelDelegate.java +++ b/src/com/android/launcher3/model/ModelDelegate.java @@ -44,13 +44,11 @@ public class ModelDelegate implements ResourceBasedOverride { boolean isPrimaryInstance) { ModelDelegate delegate = Overrides.getObject( ModelDelegate.class, context, R.string.model_delegate_class); - delegate.mApp = app; - delegate.mAppsList = appsList; - delegate.mDataModel = dataModel; - delegate.mIsPrimaryInstance = isPrimaryInstance; + delegate.init(context, app, appsList, dataModel, isPrimaryInstance); return delegate; } + protected Context mContext; protected LauncherAppState mApp; protected AllAppsList mAppsList; protected BgDataModel mDataModel; @@ -58,6 +56,18 @@ public class ModelDelegate implements ResourceBasedOverride { public ModelDelegate() { } + /** + * Initializes the object with the given params. + */ + private void init(Context context, LauncherAppState app, AllAppsList appsList, + BgDataModel dataModel, boolean isPrimaryInstance) { + this.mApp = app; + this.mAppsList = appsList; + this.mDataModel = dataModel; + this.mIsPrimaryInstance = isPrimaryInstance; + this.mContext = context; + } + /** * Called periodically to validate and update any data */ @@ -75,6 +85,14 @@ public class ModelDelegate implements ResourceBasedOverride { @WorkerThread public void loadItems(UserManagerState ums, Map pinnedShortcuts) { } + /** + * Load String cache + */ + @WorkerThread + public void loadStringCache(StringCache cache) { + cache.loadStrings(mContext); + } + /** * Called during loader after workspace loading is complete */ diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java index ef5eef1e43..422af43e19 100644 --- a/src/com/android/launcher3/model/ModelUtils.java +++ b/src/com/android/launcher3/model/ModelUtils.java @@ -23,10 +23,8 @@ import android.graphics.Bitmap; import android.os.Process; import android.util.Log; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.data.ItemInfo; @@ -53,7 +51,7 @@ public class ModelUtils { * specified screen. */ public static void filterCurrentWorkspaceItems( - IntSet currentScreenIds, + final IntSet currentScreenIds, ArrayList allWorkspaceItems, ArrayList currentScreenItems, ArrayList otherScreenItems) { @@ -91,42 +89,6 @@ public class ModelUtils { } } - /** - * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right) - */ - public static void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile, - ArrayList workspaceItems) { - final int screenCols = profile.numColumns; - final int screenCellCount = profile.numColumns * profile.numRows; - Collections.sort(workspaceItems, (lhs, rhs) -> { - if (lhs.container == rhs.container) { - // Within containers, order by their spatial position in that container - switch (lhs.container) { - case LauncherSettings.Favorites.CONTAINER_DESKTOP: { - int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols - + lhs.cellX); - int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols - + rhs.cellX); - return Integer.compare(lr, rr); - } - case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { - // We currently use the screen id as the rank - return Integer.compare(lhs.screenId, rhs.screenId); - } - default: - if (FeatureFlags.IS_STUDIO_BUILD) { - throw new RuntimeException( - "Unexpected container type when sorting workspace items."); - } - return 0; - } - } else { - // Between containers, order by hotseat, desktop - return Integer.compare(lhs.container, rhs.container); - } - }); - } - /** * Iterates though current workspace items and returns available hotseat ranks for prediction. */ diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 94e06d16ce..0a68d4aa34 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -23,8 +23,10 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.LauncherAppState; @@ -53,6 +55,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -271,28 +274,30 @@ public class ModelWriter { /** * Removes the specified item from the database */ - public void deleteItemFromDatabase(ItemInfo item) { - deleteItemsFromDatabase(Arrays.asList(item)); + public void deleteItemFromDatabase(ItemInfo item, @Nullable final String reason) { + deleteItemsFromDatabase(Arrays.asList(item), reason); } /** * Removes all the items from the database matching {@param matcher}. */ - public void deleteItemsFromDatabase(ItemInfoMatcher matcher) { + public void deleteItemsFromDatabase(@NonNull final Predicate matcher, + @Nullable final String reason) { deleteItemsFromDatabase(StreamSupport.stream(mBgDataModel.itemsIdMap.spliterator(), false) - .filter(matcher::matchesInfo) - .collect(Collectors.toList())); + .filter(matcher).collect(Collectors.toList()), reason); } /** * Removes the specified items from the database */ - public void deleteItemsFromDatabase(final Collection items) { + public void deleteItemsFromDatabase(final Collection items, + @Nullable final String reason) { ModelVerifier verifier = new ModelVerifier(); FileLog.d(TAG, "removing items from db " + items.stream().map( (item) -> item.getTargetComponent() == null ? "" : item.getTargetComponent().getPackageName()).collect( - Collectors.joining(","))); + Collectors.joining(",")) + + ". Reason: [" + (TextUtils.isEmpty(reason) ? "unknown" : reason) + "]"); notifyDelete(items); enqueueDeleteRunnable(() -> { for (ItemInfo item : items) { @@ -328,14 +333,15 @@ public class ModelWriter { /** * Deletes the widget info and the widget id. */ - public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherAppWidgetHost host) { + public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherAppWidgetHost host, + @Nullable final String reason) { notifyDelete(Collections.singleton(info)); if (host != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) { // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... enqueueDeleteRunnable(() -> host.deleteAppWidgetId(info.appWidgetId)); } - deleteItemFromDatabase(info); + deleteItemFromDatabase(info, reason); } private void notifyDelete(Collection items) { diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 83fb3d1582..489bc38376 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -57,6 +57,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.function.Predicate; /** * Handles updates due to changes in package manager (app installed/updated/removed) @@ -95,7 +96,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { final int N = packages.length; final FlagOp flagOp; final HashSet packageSet = new HashSet<>(Arrays.asList(packages)); - final ItemInfoMatcher matcher = mOp == OP_USER_AVAILABILITY_CHANGE + final Predicate matcher = mOp == OP_USER_AVAILABILITY_CHANGE ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user : ItemInfoMatcher.ofPackages(packageSet, mUser); final HashSet removedComponents = new HashSet<>(); @@ -112,7 +113,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { activitiesLists.put( packages[i], appsList.addPackage(context, packages[i], mUser)); } - flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); + flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); break; } case OP_UPDATE: @@ -134,7 +135,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } } // Since package was just updated, the target must be available now. - flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); + flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); break; case OP_REMOVE: { for (int i = 0; i < N; i++) { @@ -148,13 +149,12 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); appsList.removePackage(packages[i], mUser); } - flagOp = FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); + flagOp = FlagOp.NO_OP.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); break; case OP_SUSPEND: case OP_UNSUSPEND: - flagOp = mOp == OP_SUSPEND ? - FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED) : - FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED); + flagOp = FlagOp.NO_OP.setFlag( + WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED, mOp == OP_SUSPEND); if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N); appsList.updateDisabledFlags(matcher, flagOp); break; @@ -162,9 +162,8 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { UserManagerState ums = new UserManagerState(); ums.init(UserCache.INSTANCE.get(context), context.getSystemService(UserManager.class)); - flagOp = ums.isUserQuiet(mUser) - ? FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER) - : FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER); + flagOp = FlagOp.NO_OP.setFlag( + WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER, ums.isUserQuiet(mUser)); appsList.updateDisabledFlags(matcher, flagOp); // We are not synchronizing here, as int operations are atomic @@ -208,7 +207,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } ComponentName cn = si.getTargetComponent(); - if (cn != null && matcher.matches(si, cn)) { + if (cn != null && matcher.test(si)) { String packageName = cn.getPackageName(); if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { @@ -312,7 +311,9 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { bindUpdatedWorkspaceItems(updatedWorkspaceItems); if (!removedShortcuts.isEmpty()) { - deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts)); + deleteAndBindComponentsRemoved( + ItemInfoMatcher.ofItemIds(removedShortcuts), + "removed because the target component is invalid"); } if (!widgets.isEmpty()) { @@ -338,10 +339,11 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { - ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) + Predicate removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); - deleteAndBindComponentsRemoved(removeMatch); + deleteAndBindComponentsRemoved(removeMatch, + "removed because the corresponding package or component is removed"); // Remove any queued items from the install queue ItemInstallQueue.INSTANCE.get(context) diff --git a/src/com/android/launcher3/model/ReloadStringCacheTask.java b/src/com/android/launcher3/model/ReloadStringCacheTask.java new file mode 100644 index 0000000000..f4d42988bf --- /dev/null +++ b/src/com/android/launcher3/model/ReloadStringCacheTask.java @@ -0,0 +1,39 @@ +/* + * 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.launcher3.model; + +import com.android.launcher3.LauncherAppState; + +/** + * Handles updates due to changes in Device Policy Management resources triggered by + * {@link android.app.admin.DevicePolicyManager#ACTION_DEVICE_POLICY_RESOURCE_UPDATED}. + */ +public class ReloadStringCacheTask extends BaseModelUpdateTask { + private ModelDelegate mModelDelegate; + + public ReloadStringCacheTask(ModelDelegate modelDelegate) { + mModelDelegate = modelDelegate; + } + + @Override + public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) { + synchronized (dataModel) { + mModelDelegate.loadStringCache(dataModel.stringCache); + StringCache cloneSC = dataModel.stringCache.clone(); + scheduleCallbackTask(c -> c.bindStringCache(cloneSC)); + } + } +} diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java index 4296d32f24..1026e0bd5b 100644 --- a/src/com/android/launcher3/model/ShortcutsChangedTask.java +++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java @@ -108,7 +108,8 @@ public class ShortcutsChangedTask extends BaseModelUpdateTask { deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys( nonPinnedIds.stream() .map(id -> new ShortcutKey(mPackageName, mUser, id)) - .collect(Collectors.toSet()))); + .collect(Collectors.toSet())), + "removed because the shortcut is no longer available in shortcut service"); } } diff --git a/src/com/android/launcher3/model/StringCache.java b/src/com/android/launcher3/model/StringCache.java new file mode 100644 index 0000000000..9859ddca28 --- /dev/null +++ b/src/com/android/launcher3/model/StringCache.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import android.annotation.SuppressLint; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; + +/** + * + * Cache for the device policy strings used in Launcher. + */ +public class StringCache { + + private static final String PREFIX = "Launcher."; + + /** + * Work folder name. + */ + public static final String WORK_FOLDER_NAME = PREFIX + "WORK_FOLDER_NAME"; + + /** + * User on-boarding title for work profile apps. + */ + private static final String WORK_PROFILE_EDU = PREFIX + "WORK_PROFILE_EDU"; + + /** + * Action label to finish work profile edu. + */ + private static final String WORK_PROFILE_EDU_ACCEPT = PREFIX + "WORK_PROFILE_EDU_ACCEPT"; + + /** + * Title shown when user opens work apps tab while work profile is paused. + */ + private static final String WORK_PROFILE_PAUSED_TITLE = + PREFIX + "WORK_PROFILE_PAUSED_TITLE"; + + /** + * Description shown when user opens work apps tab while work profile is paused. + */ + private static final String WORK_PROFILE_PAUSED_DESCRIPTION = + PREFIX + "WORK_PROFILE_PAUSED_DESCRIPTION"; + + /** + * Shown on the button to pause work profile. + */ + private static final String WORK_PROFILE_PAUSE_BUTTON = + PREFIX + "WORK_PROFILE_PAUSE_BUTTON"; + + /** + * Shown on the button to enable work profile. + */ + private static final String WORK_PROFILE_ENABLE_BUTTON = + PREFIX + "WORK_PROFILE_ENABLE_BUTTON"; + + /** + * Label on launcher tab to indicate work apps. + */ + private static final String ALL_APPS_WORK_TAB = PREFIX + "ALL_APPS_WORK_TAB"; + + /** + * Label on launcher tab to indicate personal apps. + */ + private static final String ALL_APPS_PERSONAL_TAB = PREFIX + "ALL_APPS_PERSONAL_TAB"; + + /** + * Accessibility description for launcher tab to indicate work apps. + */ + private static final String ALL_APPS_WORK_TAB_ACCESSIBILITY = + PREFIX + "ALL_APPS_WORK_TAB_ACCESSIBILITY"; + + /** + * Accessibility description for launcher tab to indicate personal apps. + */ + private static final String ALL_APPS_PERSONAL_TAB_ACCESSIBILITY = + PREFIX + "ALL_APPS_PERSONAL_TAB_ACCESSIBILITY"; + + /** + * Label on widget tab to indicate work app widgets. + */ + private static final String WIDGETS_WORK_TAB = PREFIX + "WIDGETS_WORK_TAB"; + + /** + * Label on widget tab to indicate personal app widgets. + */ + private static final String WIDGETS_PERSONAL_TAB = PREFIX + "WIDGETS_PERSONAL_TAB"; + + /** + * Message shown when a feature is disabled by the admin (e.g. changing wallpaper). + */ + private static final String DISABLED_BY_ADMIN_MESSAGE = + PREFIX + "DISABLED_BY_ADMIN_MESSAGE"; + + /** + * User on-boarding title for work profile apps. + */ + public String workProfileEdu; + + /** + * Action label to finish work profile edu. + */ + public String workProfileEduAccept; + + /** + * Title shown when user opens work apps tab while work profile is paused. + */ + public String workProfilePausedTitle; + + /** + * Description shown when user opens work apps tab while work profile is paused. + */ + public String workProfilePausedDescription; + + /** + * Shown on the button to pause work profile. + */ + public String workProfilePauseButton; + + /** + * Shown on the button to enable work profile. + */ + public String workProfileEnableButton; + + /** + * Label on launcher tab to indicate work apps. + */ + public String allAppsWorkTab; + + /** + * Label on launcher tab to indicate personal apps. + */ + public String allAppsPersonalTab; + + /** + * Accessibility description for launcher tab to indicate work apps. + */ + public String allAppsWorkTabAccessibility; + + /** + * Accessibility description for launcher tab to indicate personal apps. + */ + public String allAppsPersonalTabAccessibility; + + /** + * Work folder name. + */ + public String workFolderName; + + /** + * Label on widget tab to indicate work app widgets. + */ + public String widgetsWorkTab; + + /** + * Label on widget tab to indicate personal app widgets. + */ + public String widgetsPersonalTab; + + /** + * Message shown when a feature is disabled by the admin (e.g. changing wallpaper). + */ + public String disabledByAdminMessage; + + /** + * Sets the default values for the strings. + */ + public void loadStrings(Context context) { + workProfileEdu = getEnterpriseString( + context, WORK_PROFILE_EDU, R.string.work_profile_edu_work_apps); + workProfileEduAccept = getEnterpriseString( + context, WORK_PROFILE_EDU_ACCEPT, R.string.work_profile_edu_accept); + workProfilePausedTitle = getEnterpriseString( + context, WORK_PROFILE_PAUSED_TITLE, R.string.work_apps_paused_title); + workProfilePausedDescription = getEnterpriseString( + context, WORK_PROFILE_PAUSED_DESCRIPTION, R.string.work_apps_paused_body); + workProfilePauseButton = getEnterpriseString( + context, WORK_PROFILE_PAUSE_BUTTON, R.string.work_apps_pause_btn_text); + workProfileEnableButton = getEnterpriseString( + context, WORK_PROFILE_ENABLE_BUTTON, R.string.work_apps_enable_btn_text); + allAppsWorkTab = getEnterpriseString( + context, ALL_APPS_WORK_TAB, R.string.all_apps_work_tab); + allAppsPersonalTab = getEnterpriseString( + context, ALL_APPS_PERSONAL_TAB, R.string.all_apps_personal_tab); + allAppsWorkTabAccessibility = getEnterpriseString( + context, ALL_APPS_WORK_TAB_ACCESSIBILITY, R.string.all_apps_button_work_label); + allAppsPersonalTabAccessibility = getEnterpriseString( + context, ALL_APPS_PERSONAL_TAB_ACCESSIBILITY, + R.string.all_apps_button_personal_label); + workFolderName = getEnterpriseString( + context, WORK_FOLDER_NAME, R.string.work_folder_name); + widgetsWorkTab = getEnterpriseString( + context, WIDGETS_WORK_TAB, R.string.widgets_full_sheet_work_tab); + widgetsPersonalTab = getEnterpriseString( + context, WIDGETS_PERSONAL_TAB, R.string.widgets_full_sheet_personal_tab); + disabledByAdminMessage = getEnterpriseString( + context, DISABLED_BY_ADMIN_MESSAGE, R.string.msg_disabled_by_admin); + } + + @SuppressLint("NewApi") + private String getEnterpriseString( + Context context, String updatableStringId, int defaultStringId) { + return Utilities.ATLEAST_T + ? getUpdatableEnterpriseSting(context, updatableStringId, defaultStringId) + : context.getString(defaultStringId); + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private String getUpdatableEnterpriseSting( + Context context, String updatableStringId, int defaultStringId) { + DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + return dpm.getResources().getString( + updatableStringId, () -> context.getString(defaultStringId)); + } + + @Override + public StringCache clone() { + StringCache clone = new StringCache(); + clone.workProfileEdu = this.workProfileEdu; + clone.workProfileEduAccept = this.workProfileEduAccept; + clone.workProfilePausedTitle = this.workProfilePausedTitle; + clone.workProfilePausedDescription = this.workProfilePausedDescription; + clone.workProfilePauseButton = this.workProfilePauseButton; + clone.workProfileEnableButton = this.workProfileEnableButton; + clone.allAppsWorkTab = this.allAppsWorkTab; + clone.allAppsPersonalTab = this.allAppsPersonalTab; + clone.allAppsWorkTabAccessibility = this.allAppsWorkTabAccessibility; + clone.allAppsPersonalTabAccessibility = this.allAppsPersonalTabAccessibility; + clone.workFolderName = this.workFolderName; + clone.widgetsWorkTab = this.widgetsWorkTab; + clone.widgetsPersonalTab = this.widgetsPersonalTab; + clone.disabledByAdminMessage = this.disabledByAdminMessage; + return clone; + } +} diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java index 5048e13e3e..1565b19f4c 100644 --- a/src/com/android/launcher3/model/UserLockStateChangedTask.java +++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java @@ -96,7 +96,9 @@ public class UserLockStateChangedTask extends BaseModelUpdateTask { } bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos); if (!removedKeys.isEmpty()) { - deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys(removedKeys)); + deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys(removedKeys), + "removed during unlock because it's no longer available" + + " (possibly due to clear data)"); } // Remove shortcut id map for that user diff --git a/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java new file mode 100644 index 0000000000..93fc6a539f --- /dev/null +++ b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model; + +import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; + +import android.util.LongSparseArray; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.GridOccupancy; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSet; + +import java.util.ArrayList; + +/** + * Utility class to help find space for new workspace items + */ +public class WorkspaceItemSpaceFinder { + + /** + * Find a position on the screen for the given size or adds a new screen. + * + * @return screenId and the coordinates for the item in an int array of size 3. + */ + public int[] findSpaceForItem(LauncherAppState app, BgDataModel dataModel, + IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) { + LongSparseArray> screenItems = new LongSparseArray<>(); + + // Use sBgItemsIdMap as all the items are already loaded. + synchronized (dataModel) { + for (ItemInfo info : dataModel.itemsIdMap) { + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + ArrayList items = screenItems.get(info.screenId); + if (items == null) { + items = new ArrayList<>(); + screenItems.put(info.screenId, items); + } + items.add(info); + } + } + } + + // Find appropriate space for the item. + int screenId = 0; + int[] coordinates = new int[2]; + boolean found = false; + + int screenCount = workspaceScreens.size(); + // First check the preferred screen. + IntSet screensToExclude = new IntSet(); + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + screensToExclude.add(FIRST_SCREEN_ID); + } + + for (int screen = 0; screen < screenCount; screen++) { + screenId = workspaceScreens.get(screen); + if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen( + app, screenItems.get(screenId), coordinates, spanX, spanY)) { + // We found a space for it + found = true; + break; + } + } + + if (!found) { + // Still no position found. Add a new screen to the end. + screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(), + LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) + .getInt(LauncherSettings.Settings.EXTRA_VALUE); + + // Save the screen id for binding in the workspace + workspaceScreens.add(screenId); + addedWorkspaceScreensFinal.add(screenId); + + // If we still can't find an empty space, then God help us all!!! + if (!findNextAvailableIconSpaceInScreen( + app, screenItems.get(screenId), coordinates, spanX, spanY)) { + throw new RuntimeException("Can't find space to add the item"); + } + } + return new int[]{screenId, coordinates[0], coordinates[1]}; + } + + private boolean findNextAvailableIconSpaceInScreen( + LauncherAppState app, ArrayList occupiedPos, + int[] xy, int spanX, int spanY) { + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + + GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows); + if (occupiedPos != null) { + for (ItemInfo r : occupiedPos) { + occupied.markCells(r, true); + } + } + return occupied.findVacantCell(xy, spanX, spanY); + } +} diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java index 7f70bade3c..5b2bcf5819 100644 --- a/src/com/android/launcher3/model/data/AppInfo.java +++ b/src/com/android/launcher3/model/data/AppInfo.java @@ -35,7 +35,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.pm.PackageInstallInfo; -import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; import java.util.Comparator; @@ -43,7 +42,7 @@ import java.util.Comparator; /** * Represents an app in AllAppsView. */ -public class AppInfo extends ItemInfoWithIcon { +public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { public static final AppInfo[] EMPTY_ARRAY = new AppInfo[0]; public static final Comparator COMPONENT_KEY_COMPARATOR = (a, b) -> { @@ -121,7 +120,8 @@ public class AppInfo extends ItemInfoWithIcon { return super.dumpProperties() + " componentName=" + componentName; } - public WorkspaceItemInfo makeWorkspaceItem() { + @Override + public WorkspaceItemInfo makeWorkspaceItem(Context context) { WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo(this); if ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) { @@ -139,10 +139,6 @@ public class AppInfo extends ItemInfoWithIcon { return workspaceItemInfo; } - public ComponentKey toComponentKey() { - return new ComponentKey(componentName, user); - } - public static Intent makeLaunchIntent(LauncherActivityInfo info) { return makeLaunchIntent(info.getComponentName()); } diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java index cd2ef35a66..efebce342f 100644 --- a/src/com/android/launcher3/model/data/FolderInfo.java +++ b/src/com/android/launcher3/model/data/FolderInfo.java @@ -217,7 +217,7 @@ public class FolderInfo extends ItemInfo { return getDefaultItemInfoBuilder() .setFolderIcon(folderIcon) .setRank(rank) - .setAttribute(getLabelState().mLogAttribute) + .addItemAttributes(getLabelState().mLogAttribute) .setContainerInfo(getContainerInfo()) .build(); } diff --git a/src/com/android/launcher3/model/data/IconRequestInfo.java b/src/com/android/launcher3/model/data/IconRequestInfo.java index 5dc6a3bb75..fbf01e514e 100644 --- a/src/com/android/launcher3/model/data/IconRequestInfo.java +++ b/src/com/android/launcher3/model/data/IconRequestInfo.java @@ -75,7 +75,10 @@ public class IconRequestInfo { this.useLowResIcon = useLowResIcon; } - /** Loads */ + /** + * Loads this request's item info's title. This method should only be used on IconRequestInfos + * for WorkspaceItemInfos. + */ public boolean loadWorkspaceIcon(Context context) { if (!(itemInfo instanceof WorkspaceItemInfo)) { throw new IllegalStateException( diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java index a74c02fb01..76a0c4d64c 100644 --- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java @@ -23,6 +23,7 @@ import android.content.Intent; import androidx.annotation.Nullable; import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.logging.FileLog; import com.android.launcher3.pm.PackageInstallInfo; @@ -70,10 +71,6 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ public static final int FLAG_DISABLED_LOCKED_USER = 1 << 5; - public static final int FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE - | FLAG_DISABLED_NOT_AVAILABLE | FLAG_DISABLED_SUSPENDED - | FLAG_DISABLED_QUIET_USER | FLAG_DISABLED_BY_PUBLISHER | FLAG_DISABLED_LOCKED_USER; - /** * The item points to a system app. */ @@ -112,6 +109,16 @@ public abstract class ItemInfoWithIcon extends ItemInfo { public static final int FLAG_SHOW_DOWNLOAD_PROGRESS_MASK = FLAG_INSTALL_SESSION_ACTIVE | FLAG_INCREMENTAL_DOWNLOAD_ACTIVE; + /** + * Indicates that the icon is a disabled shortcut and application updates are required. + */ + public static final int FLAG_DISABLED_VERSION_LOWER = 1 << 12; + + public static final int FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE + | FLAG_DISABLED_NOT_AVAILABLE | FLAG_DISABLED_SUSPENDED + | FLAG_DISABLED_QUIET_USER | FLAG_DISABLED_BY_PUBLISHER | FLAG_DISABLED_LOCKED_USER + | FLAG_DISABLED_VERSION_LOWER; + /** * Status associated with the system state of the underlying item. This is calculated every * time a new info is created and not persisted on the disk. @@ -230,15 +237,14 @@ public abstract class ItemInfoWithIcon extends ItemInfo { * Returns a FastBitmapDrawable with the icon. */ public FastBitmapDrawable newIcon(Context context) { - return newIcon(context, false); + return newIcon(context, 0); } /** * Returns a FastBitmapDrawable with the icon and context theme applied */ - public FastBitmapDrawable newIcon(Context context, boolean applyTheme) { - FastBitmapDrawable drawable = applyTheme - ? bitmap.newThemedIcon(context) : bitmap.newIcon(context); + public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) { + FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags); drawable.setIsDisabled(isDisabled()); return drawable; } diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java index 0283d5f398..e57a895ddd 100644 --- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java @@ -288,7 +288,7 @@ public class LauncherAppWidgetInfo extends ItemInfo { LauncherAtom.ItemInfo info = super.buildProto(folderInfo); return info.toBuilder() .setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures)) - .setAttribute(getAttribute(sourceContainer)) + .addItemAttributes(getAttribute(sourceContainer)) .build(); } } diff --git a/src/com/android/launcher3/model/data/SearchActionItemInfo.java b/src/com/android/launcher3/model/data/SearchActionItemInfo.java index c6e5e8a98f..e879313f5e 100644 --- a/src/com/android/launcher3/model/data/SearchActionItemInfo.java +++ b/src/com/android/launcher3/model/data/SearchActionItemInfo.java @@ -18,6 +18,7 @@ package com.android.launcher3.model.data; import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.os.Process; @@ -26,19 +27,14 @@ import android.os.UserHandle; import androidx.annotation.Nullable; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; -import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.logger.LauncherAtom.ItemInfo; import com.android.launcher3.logger.LauncherAtom.SearchActionItem; -import com.android.launcher3.model.AllAppsList; -import com.android.launcher3.model.BaseModelUpdateTask; -import com.android.launcher3.model.BgDataModel; /** * Represents a SearchAction with in launcher */ -public class SearchActionItemInfo extends ItemInfoWithIcon { +public class SearchActionItemInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { public static final int FLAG_SHOULD_START = 1 << 1; public static final int FLAG_SHOULD_START_FOR_RESULT = FLAG_SHOULD_START | 1 << 2; @@ -159,7 +155,8 @@ public class SearchActionItemInfo extends ItemInfoWithIcon { /** * Creates a {@link WorkspaceItemInfo} coorsponding to search action to be stored in launcher db */ - public WorkspaceItemInfo createWorkspaceItem(LauncherModel model) { + @Override + public WorkspaceItemInfo makeWorkspaceItem(Context context) { WorkspaceItemInfo info = new WorkspaceItemInfo(); info.title = title; info.bitmap = bitmap; @@ -168,20 +165,12 @@ public class SearchActionItemInfo extends ItemInfoWithIcon { if (hasFlags(FLAG_SHOULD_START_FOR_RESULT)) { info.options |= WorkspaceItemInfo.FLAG_START_FOR_RESULT; } - - model.enqueueModelUpdateTask(new BaseModelUpdateTask() { - @Override - public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { - - model.updateAndBindWorkspaceItem(() -> { - PackageItemInfo pkgInfo = new PackageItemInfo(getIntentPackageName(), user); - app.getIconCache().getTitleAndIconForApp(pkgInfo, false); - try (LauncherIcons li = LauncherIcons.obtain(app.getContext())) { - info.bitmap = li.badgeBitmap(info.bitmap.icon, pkgInfo.bitmap); - } - return info; - }); - } + LauncherAppState app = LauncherAppState.getInstance(context); + app.getModel().updateAndBindWorkspaceItem(() -> { + PackageItemInfo pkgInfo = new PackageItemInfo(getIntentPackageName(), user); + app.getIconCache().getTitleAndIconForApp(pkgInfo, false); + info.bitmap = info.bitmap.withBadgeInfo(pkgInfo.bitmap); + return info; }); return info; } diff --git a/src/com/android/launcher3/model/data/WorkspaceItemFactory.java b/src/com/android/launcher3/model/data/WorkspaceItemFactory.java new file mode 100644 index 0000000000..47b9c6e443 --- /dev/null +++ b/src/com/android/launcher3/model/data/WorkspaceItemFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model.data; + +import android.content.Context; + +/** + * Interface to objects capable of generating workspace item + */ +public interface WorkspaceItemFactory { + + /** + * Called to create a pinnable item info + */ + WorkspaceItemInfo makeWorkspaceItem(Context context); +} diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java index a195979148..2b3da335c3 100644 --- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java +++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java @@ -180,12 +180,25 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { runtimeStatusFlags |= FLAG_DISABLED_BY_PUBLISHER; } disabledMessage = shortcutInfo.getDisabledMessage(); + if (Utilities.ATLEAST_P + && shortcutInfo.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { + runtimeStatusFlags |= FLAG_DISABLED_VERSION_LOWER; + } else { + runtimeStatusFlags &= ~FLAG_DISABLED_VERSION_LOWER; + } Person[] persons = ApiWrapper.getPersons(shortcutInfo); personKeys = persons.length == 0 ? Utilities.EMPTY_STRING_ARRAY : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new); } + /** + * {@code true} if the shortcut is disabled due to its app being a lower version. + */ + public boolean isDisabledVersionLower() { + return (runtimeStatusFlags & FLAG_DISABLED_VERSION_LOWER) != 0; + } + /** Returns the WorkspaceItemInfo id associated with the deep shortcut. */ public String getDeepShortcutId() { return itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java index d27d8c7778..bb2c37f131 100644 --- a/src/com/android/launcher3/notification/NotificationInfo.java +++ b/src/com/android/launcher3/notification/NotificationInfo.java @@ -16,6 +16,8 @@ package com.android.launcher3.notification; +import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP; +import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_LAUNCH_TAP; import android.app.ActivityOptions; @@ -29,12 +31,13 @@ import android.service.notification.StatusBarNotification; import android.view.View; import com.android.launcher3.AbstractFloatingView; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.views.ActivityContext; /** * An object that contains relevant information from a {@link StatusBarNotification}. This should @@ -99,21 +102,24 @@ public class NotificationInfo implements View.OnClickListener { if (intent == null) { return; } - final Launcher launcher = Launcher.getLauncher(view.getContext()); + final ActivityContext context = ActivityContext.lookupContext(view.getContext()); Bundle activityOptions = ActivityOptions.makeClipRevealAnimation( view, 0, 0, view.getWidth(), view.getHeight()).toBundle(); try { intent.send(null, 0, null, null, null, null, activityOptions); - launcher.getStatsLogManager().logger().withItemInfo(mItemInfo) + context.getStatsLogManager().logger().withItemInfo(mItemInfo) .log(LAUNCHER_NOTIFICATION_LAUNCH_TAP); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } if (autoCancel) { - launcher.getPopupDataProvider().cancelNotification(notificationKey); + PopupDataProvider popupDataProvider = context.getPopupDataProvider(); + if (popupDataProvider != null) { + popupDataProvider.cancelNotification(notificationKey); + } } - AbstractFloatingView.closeOpenContainer(launcher, AbstractFloatingView - .TYPE_ACTION_POPUP); + AbstractFloatingView.closeOpenViews( + context, true, TYPE_ACTION_POPUP | TYPE_TASKBAR_ALL_APPS); } public Drawable getIconForBackground(Context context, int background) { diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index e58f5fa2d5..04eb38a3c5 100644 --- a/src/com/android/launcher3/notification/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -30,10 +30,12 @@ import android.os.Message; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -66,7 +68,8 @@ public class NotificationListener extends NotificationListenerService { private static final int MSG_RANKING_UPDATE = 5; private static NotificationListener sNotificationListenerInstance = null; - private static NotificationsChangedListener sNotificationsChangedListener; + private static final ArraySet sNotificationsChangedListeners = + new ArraySet<>(); private static boolean sIsConnected; private final Handler mWorkerHandler; @@ -94,8 +97,11 @@ public class NotificationListener extends NotificationListenerService { return sIsConnected ? sNotificationListenerInstance : null; } - public static void setNotificationsChangedListener(NotificationsChangedListener listener) { - sNotificationsChangedListener = listener; + public static void addNotificationsChangedListener(NotificationsChangedListener listener) { + if (listener == null) { + return; + } + sNotificationsChangedListeners.add(listener); NotificationListener notificationListener = getInstanceIfConnected(); if (notificationListener != null) { @@ -108,8 +114,10 @@ public class NotificationListener extends NotificationListenerService { } } - public static void removeNotificationsChangedListener() { - sNotificationsChangedListener = null; + public static void removeNotificationsChangedListener(NotificationsChangedListener listener) { + if (listener != null) { + sNotificationsChangedListeners.remove(listener); + } } private boolean handleWorkerMessage(Message message) { @@ -147,14 +155,9 @@ public class NotificationListener extends NotificationListenerService { case MSG_NOTIFICATION_FULL_REFRESH: List activeNotifications = null; if (sIsConnected) { - try { - activeNotifications = Arrays.stream(getActiveNotifications()) - .filter(this::notificationIsValidForUI) - .collect(Collectors.toList()); - } catch (SecurityException ex) { - Log.e(TAG, "SecurityException: failed to fetch notifications"); - activeNotifications = new ArrayList<>(); - } + activeNotifications = Arrays.stream(getActiveNotificationsSafely(null)) + .filter(this::notificationIsValidForUI) + .collect(Collectors.toList()); } else { activeNotifications = new ArrayList<>(); } @@ -168,7 +171,7 @@ public class NotificationListener extends NotificationListenerService { } case MSG_RANKING_UPDATE: { String[] keys = ((RankingMap) message.obj).getOrderedKeys(); - for (StatusBarNotification sbn : getActiveNotifications(keys)) { + for (StatusBarNotification sbn : getActiveNotificationsSafely(keys)) { updateGroupKeyIfNecessary(sbn); } return true; @@ -180,29 +183,43 @@ public class NotificationListener extends NotificationListenerService { private boolean handleUiMessage(Message message) { switch (message.what) { case MSG_NOTIFICATION_POSTED: - if (sNotificationsChangedListener != null) { + if (sNotificationsChangedListeners.size() > 0) { Pair msg = (Pair) message.obj; - sNotificationsChangedListener.onNotificationPosted( - msg.first, msg.second); + for (NotificationsChangedListener listener : sNotificationsChangedListeners) { + listener.onNotificationPosted(msg.first, msg.second); + } } break; case MSG_NOTIFICATION_REMOVED: - if (sNotificationsChangedListener != null) { + if (sNotificationsChangedListeners.size() > 0) { Pair msg = (Pair) message.obj; - sNotificationsChangedListener.onNotificationRemoved( - msg.first, msg.second); + for (NotificationsChangedListener listener : sNotificationsChangedListeners) { + listener.onNotificationRemoved(msg.first, msg.second); + } } break; case MSG_NOTIFICATION_FULL_REFRESH: - if (sNotificationsChangedListener != null) { - sNotificationsChangedListener.onNotificationFullRefresh( - (List) message.obj); + if (sNotificationsChangedListeners.size() > 0) { + for (NotificationsChangedListener listener : sNotificationsChangedListeners) { + listener.onNotificationFullRefresh( + (List) message.obj); + } } break; } return true; } + private @NonNull StatusBarNotification[] getActiveNotificationsSafely(@Nullable String[] keys) { + StatusBarNotification[] result = null; + try { + result = getActiveNotifications(keys); + } catch (SecurityException e) { + Log.e(TAG, "SecurityException: failed to fetch notifications"); + } + return result == null ? new StatusBarNotification[0] : result; + } + @Override public void onListenerConnected() { super.onListenerConnected(); @@ -302,9 +319,8 @@ public class NotificationListener extends NotificationListenerService { */ @WorkerThread public List getNotificationsForKeys(List keys) { - StatusBarNotification[] notifications = getActiveNotifications( - keys.stream().map(n -> n.notificationKey).toArray(String[]::new)); - return notifications == null ? Collections.emptyList() : Arrays.asList(notifications); + return Arrays.asList(getActiveNotificationsSafely( + keys.stream().map(n -> n.notificationKey).toArray(String[]::new))); } /** diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java index f9ff8a6e56..16a40576b8 100644 --- a/src/com/android/launcher3/notification/NotificationMainView.java +++ b/src/com/android/launcher3/notification/NotificationMainView.java @@ -38,11 +38,12 @@ import android.widget.TextView; import androidx.annotation.Nullable; -import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; /** * A {@link android.widget.FrameLayout} that contains a single notification, @@ -320,9 +321,12 @@ public class NotificationMainView extends LinearLayout { } public void onChildDismissed() { - Launcher launcher = Launcher.getLauncher(getContext()); - launcher.getPopupDataProvider().cancelNotification( - mNotificationInfo.notificationKey); - launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED); + ActivityContext activityContext = ActivityContext.lookupContext(getContext()); + PopupDataProvider popupDataProvider = activityContext.getPopupDataProvider(); + if (popupDataProvider == null) { + return; + } + popupDataProvider.cancelNotification(mNotificationInfo.notificationKey); + activityContext.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED); } } diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java index 8fafb6fdde..ec691931a1 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicator.java +++ b/src/com/android/launcher3/pageindicators/PageIndicator.java @@ -25,4 +25,25 @@ public interface PageIndicator { void setActiveMarker(int activePage); void setMarkersCount(int numMarkers); + + /** + * Sets the flag if the Page Indicator should autohide. + */ + default void setShouldAutoHide(boolean shouldAutoHide) { + // No-op by default + } + + /** + * Pauses all currently running animations. + */ + default void pauseAnimations() { + // No-op by default + } + + /** + * Force-ends all currently running or paused animations. + */ + default void skipAnimationsToEnd() { + // No-op by default + } } diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java index c685891dac..1681ea5758 100644 --- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java +++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java @@ -187,6 +187,7 @@ public class WorkspacePageIndicator extends View implements Insettable, PageIndi } } + @Override public void setShouldAutoHide(boolean shouldAutoHide) { mShouldAutoHide = shouldAutoHide; if (shouldAutoHide && mLinePaint.getAlpha() > 0) { @@ -236,6 +237,7 @@ public class WorkspacePageIndicator extends View implements Insettable, PageIndi /** * Pauses all currently running animations. */ + @Override public void pauseAnimations() { for (int i = 0; i < ANIMATOR_COUNT; i++) { if (mAnimators[i] != null) { @@ -247,6 +249,7 @@ public class WorkspacePageIndicator extends View implements Insettable, PageIndi /** * Force-ends all currently running or paused animations. */ + @Override public void skipAnimationsToEnd() { for (int i = 0; i < ANIMATOR_COUNT; i++) { if (mAnimators[i] != null) { diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java index 4b86f65039..618f926bc3 100644 --- a/src/com/android/launcher3/pm/InstallSessionHelper.java +++ b/src/com/android/launcher3/pm/InstallSessionHelper.java @@ -27,6 +27,7 @@ import android.content.pm.PackageManager; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; @@ -38,6 +39,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.ItemInstallQueue; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.MainThreadInitializedObject; @@ -142,6 +144,16 @@ public class InstallSessionHelper { if (sessionInfo == null || sessionInfo.getInstallerPackageName() == null || TextUtils.isEmpty(sessionInfo.getAppPackageName())) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verify" + + ", info=" + (sessionInfo == null) + + ", info install name" + (sessionInfo == null + ? null + : sessionInfo.getInstallerPackageName()) + + ", empty pkg name" + TextUtils.isEmpty((sessionInfo == null + ? null + : sessionInfo.getAppPackageName()))); + } return null; } String pkg = sessionInfo.getInstallerPackageName(); @@ -211,6 +223,14 @@ public class InstallSessionHelper { */ @WorkerThread void tryQueuePromiseAppIcon(PackageInstaller.SessionInfo sessionInfo) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " tryQueuePromiseAppIcon" + + ", FeatureFlags=" + FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get() + + ", SessionCommitReceiveEnabled" + SessionCommitReceiver.isEnabled(mAppContext) + + ", verifySessionInfo(sessionInfo)=" + verifySessionInfo(sessionInfo) + + ", !promiseIconAdded=" + (sessionInfo != null + && !promiseIconAddedForId(sessionInfo.getSessionId()))); + } if (FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get() && SessionCommitReceiver.isEnabled(mAppContext) && verifySessionInfo(sessionInfo) @@ -227,6 +247,20 @@ public class InstallSessionHelper { } public boolean verifySessionInfo(PackageInstaller.SessionInfo sessionInfo) { + if (TestProtocol.sDebugTracing) { + boolean appNotInstalled = sessionInfo == null + || !new PackageManagerHelper(mAppContext) + .isAppInstalled(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); + boolean labelNotEmpty = sessionInfo != null + && !TextUtils.isEmpty(sessionInfo.getAppLabel()); + Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verifySessionInfo" + + ", verify(sessionInfo)=" + verify(sessionInfo) + + ", reason=" + (sessionInfo == null ? null : sessionInfo.getInstallReason()) + + ", PackageManager.INSTALL_REASON_USER=" + PackageManager.INSTALL_REASON_USER + + ", hasIcon=" + (sessionInfo != null && sessionInfo.getAppIcon() != null) + + ", label is ! empty=" + labelNotEmpty + + " +, app not installed=" + appNotInstalled); + } return verify(sessionInfo) != null && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER && sessionInfo.getAppIcon() != null diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java index e1b3c1ae36..75cf7a845b 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -25,10 +25,12 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.os.Build; import android.os.UserHandle; +import android.util.Log; import android.util.SparseArray; import androidx.annotation.WorkerThread; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.PackageUserKey; import java.lang.ref.WeakReference; @@ -57,10 +59,19 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { public void onCreated(int sessionId) { InstallSessionHelper helper = mWeakHelper.get(); Callback callback = mWeakCallback.get(); + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, "Session created sessionId=" + sessionId + + ", callback=" + callback + + ", helper=" + helper); + } if (callback == null || helper == null) { return; } SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback); + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.MISSING_PROMISE_ICON, "Session created sessionId=" + sessionId + + ", sessionInfo=" + sessionInfo); + } if (sessionInfo != null) { callback.onInstallSessionCreated(PackageInstallInfo.fromInstallingState(sessionInfo)); } diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index b1a41093f5..196cc56be7 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -48,7 +48,7 @@ import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.widget.FrameLayout; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.InsettableFrameLayout; @@ -120,7 +120,7 @@ public abstract class ArrowPopup private final GradientDrawable mRoundedTop; private final GradientDrawable mRoundedBottom; - private Runnable mOnCloseCallback = () -> { }; + @Nullable private Runnable mOnCloseCallback = null; // The rect string of the view that the arrow is attached to, in screen reference frame. protected int mArrowColor; @@ -351,7 +351,7 @@ public abstract class ArrowPopup if (mColorExtractors == null) { return; } - Workspace workspace = launcher.getWorkspace(); + Workspace workspace = launcher.getWorkspace(); if (workspace == null) { return; } @@ -602,6 +602,7 @@ public abstract class ArrowPopup mIsAboveIcon = y > dragLayer.getTop() + insets.top; if (!mIsAboveIcon) { y = mTempRect.top + iconHeight + extraVerticalSpace; + height -= extraVerticalSpace; } // Insets are added later, so subtract them now. @@ -609,7 +610,7 @@ public abstract class ArrowPopup y -= insets.top; mGravity = 0; - if (y + height > dragLayer.getBottom() - insets.bottom) { + if ((insets.top + y + height) > (dragLayer.getBottom() - insets.bottom)) { // The container is opening off the screen, so just center it in the drag layer instead. mGravity = Gravity.CENTER_VERTICAL; // Put the container next to the icon, preferring the right side in ltr (left in rtl). @@ -766,7 +767,6 @@ public abstract class ArrowPopup } } - protected void animateClose() { if (!mIsOpen) { return; @@ -816,7 +816,9 @@ public abstract class ArrowPopup mDeferContainerRemoval = false; getPopupContainer().removeView(this); getPopupContainer().removeView(mArrow); - mOnCloseCallback.run(); + if (mOnCloseCallback != null) { + mOnCloseCallback.run(); + } if (mColorExtractors != null) { mColorExtractors.forEach(e -> e.setListener(null)); } @@ -825,7 +827,7 @@ public abstract class ArrowPopup /** * Callback to be called when the popup is closed */ - public void setOnCloseCallback(@NonNull Runnable callback) { + public void setOnCloseCallback(@Nullable Runnable callback) { mOnCloseCallback = callback; } diff --git a/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java new file mode 100644 index 0000000000..c0a04b1302 --- /dev/null +++ b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.popup; + +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.model.data.ItemInfo; + +/** + * Utility class to handle updates while the popup is visible on the Launcher + */ +public class LauncherPopupLiveUpdateHandler extends PopupLiveUpdateHandler { + + public LauncherPopupLiveUpdateHandler( + Launcher launcher, PopupContainerWithArrow popupContainerWithArrow) { + super(launcher, popupContainerWithArrow); + } + + private View getWidgetsView(ViewGroup container) { + for (int i = container.getChildCount() - 1; i >= 0; --i) { + View systemShortcutView = container.getChildAt(i); + if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { + return systemShortcutView; + } + } + return null; + } + + @Override + public void onWidgetsBound() { + BubbleTextView originalIcon = mPopupContainerWithArrow.getOriginalIcon(); + SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mContext, + (ItemInfo) originalIcon.getTag(), originalIcon); + View widgetsView = getWidgetsView(mPopupContainerWithArrow); + if (widgetsView == null && mPopupContainerWithArrow.getWidgetContainer() != null) { + widgetsView = getWidgetsView(mPopupContainerWithArrow.getWidgetContainer()); + } + + if (widgetInfo != null && widgetsView == null) { + // We didn't have any widgets cached but now there are some, so enable the shortcut. + if (mPopupContainerWithArrow.getSystemShortcutContainer() + != mPopupContainerWithArrow) { + if (mPopupContainerWithArrow.getWidgetContainer() == null) { + mPopupContainerWithArrow.setWidgetContainer( + mPopupContainerWithArrow.inflateAndAdd( + R.layout.widget_shortcut_container, + mPopupContainerWithArrow)); + } + mPopupContainerWithArrow.initializeWidgetShortcut( + mPopupContainerWithArrow.getWidgetContainer(), + widgetInfo); + } else { + // If using the expanded system shortcut (as opposed to just the icon), we need + // to reopen the container to ensure measurements etc. all work out. While this + // could be quite janky, in practice the user would typically see a small + // flicker as the animation restarts partway through, and this is a very rare + // edge case anyway. + mPopupContainerWithArrow.close(false); + PopupContainerWithArrow.showForIcon(mPopupContainerWithArrow.getOriginalIcon()); + } + } else if (widgetInfo == null && widgetsView != null) { + // No widgets exist, but we previously added the shortcut so remove it. + if (mPopupContainerWithArrow.getSystemShortcutContainer() + != mPopupContainerWithArrow + && mPopupContainerWithArrow.getWidgetContainer() != null) { + mPopupContainerWithArrow.getWidgetContainer().removeView(widgetsView); + } else { + mPopupContainerWithArrow.close(false); + PopupContainerWithArrow.showForIcon(mPopupContainerWithArrow.getOriginalIcon()); + } + } + } + + @Override + protected void showPopupContainerForIcon(BubbleTextView originalIcon) { + PopupContainerWithArrow.showForIcon(originalIcon); + } +} diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 6d2b12f451..49d97d21c1 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -60,7 +60,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationContainer; import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.notification.NotificationKeyData; -import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; import com.android.launcher3.touch.ItemLongClickListener; @@ -72,9 +71,7 @@ import com.android.launcher3.views.BaseDragLayer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -93,6 +90,7 @@ public class PopupContainerWithArrow private BubbleTextView mOriginalIcon; private int mNumNotifications; private NotificationContainer mNotificationContainer; + private int mContainerWidth; private ViewGroup mWidgetContainer; @@ -107,6 +105,7 @@ public class PopupContainerWithArrow super(context, attrs, defStyleAttr); mStartDragThreshold = getResources().getDimensionPixelSize( R.dimen.deep_shortcuts_start_drag_threshold); + mContainerWidth = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_width); } public PopupContainerWithArrow(Context context, AttributeSet attrs) { @@ -151,10 +150,13 @@ public class PopupContainerWithArrow public OnClickListener getItemClickListener() { return (view) -> { mActivityContext.getItemOnClickListener().onClick(view); - close(true); }; } + public void setPopupItemDragHandler(PopupItemDragHandler popupItemDragHandler) { + mPopupItemDragHandler = popupItemDragHandler; + } + public PopupItemDragHandler getItemDragHandler() { return mPopupItemDragHandler; } @@ -185,7 +187,10 @@ public class PopupContainerWithArrow /** * Returns true if we can show the container. + * + * @deprecated Left here since some dependent projects are using this method */ + @Deprecated public static boolean canShow(View icon, ItemInfo item) { return icon instanceof BubbleTextView && ShortcutUtil.supportsShortcuts(item); } @@ -202,7 +207,7 @@ public class PopupContainerWithArrow return null; } ItemInfo item = (ItemInfo) icon.getTag(); - if (!canShow(icon, item)) { + if (!ShortcutUtil.supportsShortcuts(item)) { return null; } @@ -216,7 +221,7 @@ public class PopupContainerWithArrow popupDataProvider.getShortcutCountForItem(item), popupDataProvider.getNotificationKeysForItem(item), launcher.getSupportedShortcuts() - .map(s -> s.getShortcut(launcher, item)) + .map(s -> s.getShortcut(launcher, item, icon)) .filter(Objects::nonNull) .collect(Collectors.toList())); launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item)); @@ -225,7 +230,8 @@ public class PopupContainerWithArrow } private void configureForLauncher(Launcher launcher) { - addOnAttachStateChangeListener(new LiveUpdateHandler(launcher)); + addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler( + launcher, (PopupContainerWithArrow) this)); mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this); mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher); launcher.getDragController().addDragListener(this); @@ -245,14 +251,15 @@ public class PopupContainerWithArrow mOriginalIcon = originalIcon; boolean hasDeepShortcuts = shortcutCount > 0; - int containerWidth = (int) getResources().getDimension(R.dimen.bg_popup_item_width); + mContainerWidth = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_width); // if there are deep shortcuts, we might want to increase the width of shortcuts to fit // horizontally laid out system shortcuts. if (hasDeepShortcuts) { - containerWidth = (int) Math.max(containerWidth, - systemShortcuts.size() * getResources().getDimension( - R.dimen.system_shortcut_header_icon_touch_size)); + mContainerWidth = Math.max(mContainerWidth, + systemShortcuts.size() * getResources() + .getDimensionPixelSize(R.dimen.system_shortcut_header_icon_touch_size) + ); } // Add views if (mNumNotifications > 0) { @@ -276,7 +283,7 @@ public class PopupContainerWithArrow for (int i = shortcutCount; i > 0; i--) { DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, mDeepShortcutContainer); - v.getLayoutParams().width = containerWidth; + v.getLayoutParams().width = mContainerWidth; mShortcuts.add(v); } updateHiddenShortcuts(); @@ -288,8 +295,7 @@ public class PopupContainerWithArrow mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container, this); } - initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer, - shortcut); + initializeWidgetShortcut(mWidgetContainer, shortcut); } } mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this); @@ -329,6 +335,26 @@ public class PopupContainerWithArrow this, mShortcuts, notificationKeys)); } + protected NotificationContainer getNotificationContainer() { + return mNotificationContainer; + } + + protected BubbleTextView getOriginalIcon() { + return mOriginalIcon; + } + + protected ViewGroup getSystemShortcutContainer() { + return mSystemShortcutContainer; + } + + protected ViewGroup getWidgetContainer() { + return mWidgetContainer; + } + + protected void setWidgetContainer(ViewGroup widgetContainer) { + mWidgetContainer = widgetContainer; + } + private String getTitleForAccessibility() { return getContext().getString(mNumNotifications == 0 ? R.string.action_deep_shortcut : @@ -352,7 +378,7 @@ public class PopupContainerWithArrow } } - private void updateHiddenShortcuts() { + protected void updateHiddenShortcuts() { int allowedCount = mNotificationContainer != null ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; @@ -363,7 +389,12 @@ public class PopupContainerWithArrow } } - private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) { + protected void initializeWidgetShortcut(ViewGroup container, SystemShortcut info) { + View view = initializeSystemShortcut(R.layout.system_shortcut, container, info); + view.getLayoutParams().width = mContainerWidth; + } + + protected View initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) { View view = inflateAndAdd( resId, container, getInsertIndexForSystemShortcut(container, info)); if (view instanceof DeepShortcutView) { @@ -377,6 +408,7 @@ public class PopupContainerWithArrow } view.setTag(info); view.setOnClickListener(info); + return view; } /** @@ -442,7 +474,7 @@ public class PopupContainerWithArrow }; } - private void updateNotificationHeader() { + protected void updateNotificationHeader() { ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag(); DotInfo dotInfo = mActivityContext.getDotInfoForItem(itemInfo); if (mNotificationContainer != null && dotInfo != null) { @@ -503,119 +535,13 @@ public class PopupContainerWithArrow return getOpenView(context, TYPE_ACTION_POPUP); } - /** - * Utility class to handle updates while the popup is visible (like widgets and - * notification changes) - */ - private class LiveUpdateHandler implements - PopupDataChangeListener, View.OnAttachStateChangeListener { - - private final Launcher mLauncher; - - LiveUpdateHandler(Launcher launcher) { - mLauncher = launcher; - } - - @Override - public void onViewAttachedToWindow(View view) { - mLauncher.getPopupDataProvider().setChangeListener(this); - } - - @Override - public void onViewDetachedFromWindow(View view) { - mLauncher.getPopupDataProvider().setChangeListener(null); - } - - private View getWidgetsView(ViewGroup container) { - for (int i = container.getChildCount() - 1; i >= 0; --i) { - View systemShortcutView = container.getChildAt(i); - if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { - return systemShortcutView; - } - } - return null; - } - - @Override - public void onWidgetsBound() { - ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); - SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo); - View widgetsView = getWidgetsView(PopupContainerWithArrow.this); - if (widgetsView == null && mWidgetContainer != null) { - widgetsView = getWidgetsView(mWidgetContainer); - } - - if (widgetInfo != null && widgetsView == null) { - // We didn't have any widgets cached but now there are some, so enable the shortcut. - if (mSystemShortcutContainer != PopupContainerWithArrow.this) { - if (mWidgetContainer == null) { - mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container, - PopupContainerWithArrow.this); - } - initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer, - widgetInfo); - } else { - // If using the expanded system shortcut (as opposed to just the icon), we need - // to reopen the container to ensure measurements etc. all work out. While this - // could be quite janky, in practice the user would typically see a small - // flicker as the animation restarts partway through, and this is a very rare - // edge case anyway. - close(false); - PopupContainerWithArrow.showForIcon(mOriginalIcon); - } - } else if (widgetInfo == null && widgetsView != null) { - // No widgets exist, but we previously added the shortcut so remove it. - if (mSystemShortcutContainer - != PopupContainerWithArrow.this - && mWidgetContainer != null) { - mWidgetContainer.removeView(widgetsView); - } else { - close(false); - PopupContainerWithArrow.showForIcon(mOriginalIcon); - } - } - } - - /** - * Updates the notification header if the original icon's dot updated. - */ - @Override - public void onNotificationDotsUpdated(Predicate updatedDots) { - ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); - PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo); - if (updatedDots.test(packageUser)) { - updateNotificationHeader(); - } - } - - - @Override - public void trimNotifications(Map updatedDots) { - if (mNotificationContainer == null) { - return; - } - ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); - DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo)); - if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) { - // No more notifications, remove the notification views and expand all shortcuts. - mNotificationContainer.setVisibility(GONE); - updateHiddenShortcuts(); - assignMarginsAndBackgrounds(PopupContainerWithArrow.this); - updateArrowColor(); - } else { - mNotificationContainer.trimNotifications( - NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys())); - } - } - } - /** * Dismisses the popup if it is no longer valid */ public static void dismissInvalidPopup(BaseDraggingActivity activity) { PopupContainerWithArrow popup = getOpen(activity); if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow() - || !canShow(popup.mOriginalIcon, (ItemInfo) popup.mOriginalIcon.getTag()))) { + || !ShortcutUtil.supportsShortcuts((ItemInfo) popup.mOriginalIcon.getTag()))) { popup.animateClose(); } } diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index 6f9f0d7867..80ffecca2e 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -264,6 +264,13 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos); } + /** + * Tells the listener that the system shortcuts have been updated, causing them to be redrawn. + */ + public void redrawSystemShortcuts() { + mChangeListener.onSystemShortcutsUpdated(); + } + public interface PopupDataChangeListener { PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { }; @@ -276,5 +283,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan /** A callback to get notified when recommended widgets are bound. */ default void onRecommendedWidgetsBound() { } + + /** A callback to get notified when system shortcuts have been updated. */ + default void onSystemShortcutsUpdated() { } } } diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java new file mode 100644 index 0000000000..c5d545225c --- /dev/null +++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.popup; + +import static android.view.View.GONE; + +import android.content.Context; +import android.view.View; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.dot.DotInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.notification.NotificationContainer; +import com.android.launcher3.notification.NotificationKeyData; +import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.views.ActivityContext; + +import java.util.Map; +import java.util.function.Predicate; + +/** + * Utility class to handle updates while the popup is visible (like widgets and + * notification changes) + * + * @param The activity on which the popup shows + */ +public abstract class PopupLiveUpdateHandler implements + PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener { + + protected final T mContext; + protected final PopupContainerWithArrow mPopupContainerWithArrow; + + public PopupLiveUpdateHandler( + T context, PopupContainerWithArrow popupContainerWithArrow) { + mContext = context; + mPopupContainerWithArrow = popupContainerWithArrow; + } + + @Override + public void onViewAttachedToWindow(View view) { + PopupDataProvider popupDataProvider = mContext.getPopupDataProvider(); + + if (popupDataProvider != null) { + popupDataProvider.setChangeListener(this); + } + } + + @Override + public void onViewDetachedFromWindow(View view) { + PopupDataProvider popupDataProvider = mContext.getPopupDataProvider(); + + if (popupDataProvider != null) { + popupDataProvider.setChangeListener(null); + } + } + + /** + * Updates the notification header if the original icon's dot updated. + */ + @Override + public void onNotificationDotsUpdated(Predicate updatedDots) { + ItemInfo itemInfo = (ItemInfo) mPopupContainerWithArrow.getOriginalIcon().getTag(); + PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo); + if (updatedDots.test(packageUser)) { + mPopupContainerWithArrow.updateNotificationHeader(); + } + } + + + @Override + public void trimNotifications(Map updatedDots) { + NotificationContainer notificationContainer = + mPopupContainerWithArrow.getNotificationContainer(); + if (notificationContainer == null) { + return; + } + ItemInfo originalInfo = (ItemInfo) mPopupContainerWithArrow.getOriginalIcon().getTag(); + DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo)); + if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) { + // No more notifications, remove the notification views and expand all shortcuts. + notificationContainer.setVisibility(GONE); + mPopupContainerWithArrow.updateHiddenShortcuts(); + mPopupContainerWithArrow.assignMarginsAndBackgrounds(mPopupContainerWithArrow); + mPopupContainerWithArrow.updateArrowColor(); + } else { + notificationContainer.trimNotifications( + NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys())); + } + } + + @Override + public void onSystemShortcutsUpdated() { + mPopupContainerWithArrow.close(true); + showPopupContainerForIcon(mPopupContainerWithArrow.getOriginalIcon()); + } + + protected abstract void showPopupContainerForIcon(BubbleTextView originalIcon); +} diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java index 1dce1f25e8..8be4e6c8d9 100644 --- a/src/com/android/launcher3/popup/PopupPopulator.java +++ b/src/com/android/launcher3/popup/PopupPopulator.java @@ -162,7 +162,7 @@ public class PopupPopulator { for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) { final ShortcutInfo shortcut = shortcuts.get(i); final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, context); - cache.getUnbadgedShortcutIcon(si, shortcut); + cache.getShortcutIcon(si, shortcut); si.rank = i; si.container = CONTAINER_SHORTCUTS; diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java index 7c393ad1e2..e5e2c350b4 100644 --- a/src/com/android/launcher3/popup/RemoteActionShortcut.java +++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java @@ -46,8 +46,8 @@ public class RemoteActionShortcut extends SystemShortcut { private final RemoteAction mAction; public RemoteActionShortcut(RemoteAction action, - BaseDraggingActivity activity, ItemInfo itemInfo) { - super(0, R.id.action_remote_action_shortcut, activity, itemInfo); + BaseDraggingActivity activity, ItemInfo itemInfo, View originalView) { + super(0, R.id.action_remote_action_shortcut, activity, itemInfo, originalView); mAction = action; } diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java index af872750a0..0e25984c1b 100644 --- a/src/com/android/launcher3/popup/SystemShortcut.java +++ b/src/com/android/launcher3/popup/SystemShortcut.java @@ -46,18 +46,21 @@ public abstract class SystemShortcut extend protected final T mTarget; protected final ItemInfo mItemInfo; + protected final View mOriginalView; /** * Indicates if it's invokable or not through some disabled UI */ private boolean isEnabled = true; - public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) { + public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo, + View originalView) { mIconResId = iconResId; mLabelResId = labelResId; mAccessibilityActionId = labelResId; mTarget = target; mItemInfo = itemInfo; + mOriginalView = originalView; } public SystemShortcut(SystemShortcut other) { @@ -66,6 +69,7 @@ public abstract class SystemShortcut extend mAccessibilityActionId = other.mAccessibilityActionId; mTarget = other.mTarget; mItemInfo = other.mItemInfo; + mOriginalView = other.mOriginalView; } /** @@ -77,12 +81,15 @@ public abstract class SystemShortcut extend public void setIconAndLabelFor(View iconView, TextView labelView) { iconView.setBackgroundResource(mIconResId); + iconView.setEnabled(isEnabled); labelView.setText(mLabelResId); + labelView.setEnabled(isEnabled); } public void setIconAndContentDescriptionFor(ImageView view) { view.setImageResource(mIconResId); view.setContentDescription(view.getContext().getText(mLabelResId)); + view.setEnabled(isEnabled); } public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) { @@ -104,10 +111,10 @@ public abstract class SystemShortcut extend public interface Factory { - @Nullable SystemShortcut getShortcut(T activity, ItemInfo itemInfo); + @Nullable SystemShortcut getShortcut(T activity, ItemInfo itemInfo, View originalView); } - public static final Factory WIDGETS = (launcher, itemInfo) -> { + public static final Factory WIDGETS = (launcher, itemInfo, originalView) -> { if (itemInfo.getTargetComponent() == null) return null; final List widgets = launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey( @@ -115,12 +122,13 @@ public abstract class SystemShortcut extend if (widgets.isEmpty()) { return null; } - return new Widgets(launcher, itemInfo); + return new Widgets(launcher, itemInfo, originalView); }; public static class Widgets extends SystemShortcut { - public Widgets(Launcher target, ItemInfo itemInfo) { - super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo); + public Widgets(Launcher target, ItemInfo itemInfo, View originalView) { + super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo, + originalView); } @Override @@ -142,9 +150,9 @@ public abstract class SystemShortcut extend @Nullable private SplitAccessibilityInfo mSplitA11yInfo; - public AppInfo(T target, ItemInfo itemInfo) { + public AppInfo(T target, ItemInfo itemInfo, View originalView) { super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target, - itemInfo); + itemInfo, originalView); } /** @@ -157,8 +165,9 @@ public abstract class SystemShortcut extend * That way it could directly create the correct node info for any shortcut that supports * split, but then we'll need custom resIDs for each pair of shortcuts. */ - public AppInfo(T target, ItemInfo itemInfo, SplitAccessibilityInfo accessibilityInfo) { - this(target, itemInfo); + public AppInfo(T target, ItemInfo itemInfo, View originalView, + SplitAccessibilityInfo accessibilityInfo) { + this(target, itemInfo, originalView); mSplitA11yInfo = accessibilityInfo; mAccessibilityActionId = accessibilityInfo.nodeId; } @@ -200,28 +209,29 @@ public abstract class SystemShortcut extend } } - public static final Factory INSTALL = (activity, itemInfo) -> { - boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) - && ((WorkspaceItemInfo) itemInfo).hasStatusFlag( + public static final Factory INSTALL = + (activity, itemInfo, originalView) -> { + boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) + && ((WorkspaceItemInfo) itemInfo).hasStatusFlag( WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI); - boolean isInstantApp = false; - if (itemInfo instanceof com.android.launcher3.model.data.AppInfo) { - com.android.launcher3.model.data.AppInfo - appInfo = (com.android.launcher3.model.data.AppInfo) itemInfo; - isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo); - } - boolean enabled = supportsWebUI || isInstantApp; - if (!enabled) { - return null; - } - return new Install(activity, itemInfo); + boolean isInstantApp = false; + if (itemInfo instanceof com.android.launcher3.model.data.AppInfo) { + com.android.launcher3.model.data.AppInfo + appInfo = (com.android.launcher3.model.data.AppInfo) itemInfo; + isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo); + } + boolean enabled = supportsWebUI || isInstantApp; + if (!enabled) { + return null; + } + return new Install(activity, itemInfo, originalView); }; public static class Install extends SystemShortcut { - public Install(BaseDraggingActivity target, ItemInfo itemInfo) { + public Install(BaseDraggingActivity target, ItemInfo itemInfo, View originalView) { super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label, - target, itemInfo); + target, itemInfo, originalView); } @Override diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index d994dbec77..48b3acfbf1 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -79,11 +79,15 @@ public class RestoreDbTask { helper.createEmptyDB(helper.getWritableDatabase()); } + // Obtain InvariantDeviceProfile first before setting pending to false, so + // InvariantDeviceProfile won't switch to new grid when initializing. + InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); + // Set is pending to false irrespective of the result, so that it doesn't get // executed again. Utilities.getPrefs(context).edit().remove(RESTORED_DEVICE_TYPE).commit(); - InvariantDeviceProfile.INSTANCE.get(context).reinitializeAfterRestore(context); + idp.reinitializeAfterRestore(context); } private static boolean performRestore(Context context, DatabaseHelper helper) { diff --git a/src/com/android/launcher3/search/SearchCallback.java b/src/com/android/launcher3/search/SearchCallback.java index 5796116963..495a303a7e 100644 --- a/src/com/android/launcher3/search/SearchCallback.java +++ b/src/com/android/launcher3/search/SearchCallback.java @@ -31,13 +31,6 @@ public interface SearchCallback { */ void onSearchResult(String query, ArrayList items); - /** - * Called when the search from secondary source is complete. - * - * @param items list of search results - */ - void onAppendSearchResult(String query, ArrayList items); - /** * Called when the search results should be cleared. */ diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java index e9058c343d..a0ed77e38f 100644 --- a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java +++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java @@ -205,8 +205,8 @@ public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreference /** * Returns a system shortcut to pin/unpin a shortcut */ - public SystemShortcut getSystemShortcut(ItemInfo info) { - return new PinUnPinShortcut(mLauncher, info, + public SystemShortcut getSystemShortcut(ItemInfo info, View originalView) { + return new PinUnPinShortcut(mLauncher, info, originalView, mPinnedApps.contains(new ComponentKey(info.getTargetComponent(), info.user))); } @@ -214,10 +214,11 @@ public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreference private final boolean mIsPinned; - PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, boolean isPinned) { + PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, View originalView, + boolean isPinned) { super(isPinned ? R.drawable.ic_remove_no_shadow : R.drawable.ic_pin, isPinned ? R.string.remove_drop_target_label : R.string.action_add_to_workspace, - target, info); + target, info, originalView); mIsPinned = isPinned; } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index 1a96c2311c..a2ab7f9d53 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -30,8 +30,9 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; @@ -52,13 +53,15 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity private LauncherModel mModel; private BaseDragLayer mDragLayer; - private AllAppsContainerView mAppsView; + private ActivityAllAppsContainerView mAppsView; private View mAppsButton; private PopupDataProvider mPopupDataProvider; private boolean mAppDrawerShown = false; + private StringCache mStringCache; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -143,7 +146,8 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity return mAppDrawerShown; } - public AllAppsContainerView getAppsView() { + @Override + public ActivityAllAppsContainerView getAppsView() { return mAppsView; } @@ -225,6 +229,16 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity PopupContainerWithArrow.dismissInvalidPopup(this); } + @Override + public StringCache getStringCache() { + return mStringCache; + } + + @Override + public void bindStringCache(StringCache cache) { + mStringCache = cache; + } + public PopupDataProvider getPopupDataProvider() { return mPopupDataProvider; } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java index 1820933509..c79d70dacc 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java @@ -30,9 +30,10 @@ import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; +import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; @@ -46,7 +47,7 @@ import java.util.Collections; public class SecondaryDragLayer extends BaseDragLayer { private View mAllAppsButton; - private AllAppsContainerView mAppsView; + private ActivityAllAppsContainerView mAppsView; private GridView mWorkspace; private PinnedAppsAdapter mPinnedAppsAdapter; @@ -112,26 +113,27 @@ public class SecondaryDragLayer extends BaseDragLayer for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child == mAppsView) { - int padding = 2 * (grid.desiredWorkspaceHorizontalMarginPx - + grid.cellLayoutPaddingLeftRightPx); + int horizontalPadding = (2 * grid.desiredWorkspaceHorizontalMarginPx) + + grid.cellLayoutPaddingPx.left + grid.cellLayoutPaddingPx.right; + int verticalPadding = + grid.cellLayoutPaddingPx.top + grid.cellLayoutPaddingPx.bottom; - int maxWidth = grid.allAppsCellWidthPx * grid.numShownAllAppsColumns + padding; - int appsWidth = Math.min(width, maxWidth); + int maxWidth = + grid.allAppsCellWidthPx * grid.numShownAllAppsColumns + horizontalPadding; + int appsWidth = Math.min(width - getPaddingLeft() - getPaddingRight(), maxWidth); - int maxHeight = grid.allAppsCellHeightPx * grid.numShownAllAppsColumns + padding; - int appsHeight = Math.min(height, maxHeight); + int maxHeight = + grid.allAppsCellHeightPx * grid.numShownAllAppsColumns + verticalPadding; + int appsHeight = Math.min(height - getPaddingTop() - getPaddingBottom(), maxHeight); mAppsView.measure( makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY)); - } else if (child == mAllAppsButton) { int appsButtonSpec = makeMeasureSpec(grid.iconSizePx, EXACTLY); mAllAppsButton.measure(appsButtonSpec, appsButtonSpec); - } else if (child == mWorkspace) { measureChildWithMargins(mWorkspace, widthMeasureSpec, 0, heightMeasureSpec, grid.iconSizePx + grid.edgeMarginPx); - } else { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } @@ -177,15 +179,19 @@ public class SecondaryDragLayer extends BaseDragLayer if (!ShortcutUtil.supportsShortcuts(item)) { return false; } + PopupDataProvider popupDataProvider = mActivity.getPopupDataProvider(); + if (popupDataProvider == null) { + return false; + } final PopupContainerWithArrow container = (PopupContainerWithArrow) mActivity.getLayoutInflater().inflate( R.layout.popup_container, mActivity.getDragLayer(), false); container.populateAndShow((BubbleTextView) v, - mActivity.getPopupDataProvider().getShortcutCountForItem(item), + popupDataProvider.getShortcutCountForItem(item), Collections.emptyList(), - Arrays.asList(mPinnedAppsAdapter.getSystemShortcut(item), - APP_INFO.getShortcut(mActivity, item))); + Arrays.asList(mPinnedAppsAdapter.getSystemShortcut(item, v), + APP_INFO.getShortcut(mActivity, item, v))); v.getParent().requestDisallowInterceptTouchEvent(true); return true; } diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java index 0ee27443ef..1816e7be38 100644 --- a/src/com/android/launcher3/settings/NotificationDotsPreference.java +++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java @@ -89,6 +89,7 @@ public class NotificationDotsPreference extends Preference // Update intent Bundle extras = new Bundle(); extras.putString(EXTRA_FRAGMENT_ARG_KEY, "notification_badging"); + setIntent(new Intent("android.settings.NOTIFICATION_SETTINGS") .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, extras)); } diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java index 0c39632708..49d27b7ed4 100644 --- a/src/com/android/launcher3/settings/SettingsActivity.java +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -213,6 +213,14 @@ public class SettingsActivity extends FragmentActivity } if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) { + if (getPreferenceScreen().getTitle().equals( + getResources().getString(R.string.search_pref_screen_title))){ + DeviceProfile mDeviceProfile = InvariantDeviceProfile.INSTANCE.get( + getContext()).getDeviceProfile(getContext()); + getPreferenceScreen().setTitle(mDeviceProfile.isTablet ? + R.string.search_pref_screen_title_tablet + : R.string.search_pref_screen_title); + } getActivity().setTitle(getPreferenceScreen().getTitle()); } } diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java index 71d288c0ab..2f17ce0260 100644 --- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java +++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java @@ -72,6 +72,7 @@ public class DeepShortcutView extends FrameLayout implements BubbleTextHolder { protected void onFinishInflate() { super.onFinishInflate(); mBubbleText = findViewById(R.id.bubble_text); + mBubbleText.setHideBadge(true); mIconView = findViewById(R.id.icon); tryUpdateTextBackground(); } diff --git a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java index cecbb0db13..c166bfc83a 100644 --- a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java +++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java @@ -23,12 +23,12 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View; -import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.DragPreviewProvider; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.icons.FastBitmapDrawable; +import com.android.launcher3.views.ActivityContext; /** * Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size. @@ -45,7 +45,8 @@ public class ShortcutDragPreviewProvider extends DragPreviewProvider { @Override public Drawable createDrawable() { if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) { - int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx; + int size = ActivityContext.lookupContext(mView.getContext()) + .getDeviceProfile().iconSizePx; return new FastBitmapDrawable( BitmapRenderer.createHardwareBitmap( size + blurSizeOutline, @@ -59,7 +60,7 @@ public class ShortcutDragPreviewProvider extends DragPreviewProvider { private Bitmap createDragBitmapLegacy() { Drawable d = mView.getBackground(); Rect bounds = getDrawableBounds(d); - int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx; + int size = ActivityContext.lookupContext(mView.getContext()).getDeviceProfile().iconSizePx; final Bitmap b = Bitmap.createBitmap( size + blurSizeOutline, size + blurSizeOutline, @@ -84,9 +85,9 @@ public class ShortcutDragPreviewProvider extends DragPreviewProvider { @Override public float getScaleAndPosition(Drawable preview, int[] outPos) { - Launcher launcher = Launcher.getLauncher(mView.getContext()); + ActivityContext context = ActivityContext.lookupContext(mView.getContext()); int iconSize = getDrawableBounds(mView.getBackground()).width(); - float scale = launcher.getDragLayer().getLocationInDragLayer(mView, outPos); + float scale = context.getDragLayer().getLocationInDragLayer(mView, outPos); int iconLeft = mView.getPaddingStart(); if (Utilities.isRtl(mView.getResources())) { @@ -98,7 +99,7 @@ public class ShortcutDragPreviewProvider extends DragPreviewProvider { + mPositionShift.x); outPos[1] += Math.round((scale * mView.getHeight() - preview.getIntrinsicHeight()) / 2 + mPositionShift.y); - float size = launcher.getDeviceProfile().iconSizePx; + float size = context.getDeviceProfile().iconSizePx; return scale * iconSize / size; } } diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java index 0c6d675afd..9af68c0388 100644 --- a/src/com/android/launcher3/shortcuts/ShortcutKey.java +++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java @@ -57,11 +57,17 @@ public class ShortcutKey extends ComponentKey { } public static Intent makeIntent(ShortcutInfo si) { + return makeIntent(si.getId(), si.getPackage()).setComponent(si.getActivity()); + } + + /** + * Creates an intent for shortcut id and package name. + */ + public static Intent makeIntent(String shortcutId, String packageName) { return new Intent(Intent.ACTION_MAIN) .addCategory(INTENT_CATEGORY) - .setComponent(si.getActivity()) - .setPackage(si.getPackage()) + .setPackage(packageName) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - .putExtra(EXTRA_SHORTCUT_ID, si.getId()); + .putExtra(EXTRA_SHORTCUT_ID, shortcutId); } } diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java index 122573cfa1..f9a36ad17c 100644 --- a/src/com/android/launcher3/statemanager/BaseState.java +++ b/src/com/android/launcher3/statemanager/BaseState.java @@ -18,6 +18,7 @@ package com.android.launcher3.statemanager; import android.content.Context; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DeviceProfile.DeviceProfileListenable; /** * Interface representing a state of a StatefulActivity @@ -36,7 +37,8 @@ public interface BaseState { /** * @return How long the animation to this state should take (or from this state to NORMAL). */ - int getTransitionDuration(Context context); + + int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState); /** * Returns the state to go back to from this state diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java index 1767939114..2aa9ddeb50 100644 --- a/src/com/android/launcher3/statemanager/StateManager.java +++ b/src/com/android/launcher3/statemanager/StateManager.java @@ -253,8 +253,8 @@ public class StateManager> { // Since state mBaseState can be reached from multiple states, just assume that the // transition plays in reverse and use the same duration as previous state. mConfig.duration = state == mBaseState - ? fromState.getTransitionDuration(mActivity) - : state.getTransitionDuration(mActivity); + ? fromState.getTransitionDuration(mActivity, false /* isToState */) + : state.getTransitionDuration(mActivity, true /* isToState */); prepareForAtomicAnimation(fromState, state, mConfig); AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim(); if (listener != null) { diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java index e03694321e..c554d069f3 100644 --- a/src/com/android/launcher3/statemanager/StatefulActivity.java +++ b/src/com/android/launcher3/statemanager/StatefulActivity.java @@ -17,15 +17,11 @@ package com.android.launcher3.statemanager; import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE; -import android.graphics.Insets; -import android.os.Build; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; -import android.view.WindowInsets; import androidx.annotation.CallSuper; -import androidx.annotation.RequiresApi; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.LauncherRootView; @@ -152,7 +148,7 @@ public abstract class StatefulActivity> /** * Called if the Activity UI changed while the activity was not visible */ - protected void onUiChangedWhileSleeping() { } + public void onUiChangedWhileSleeping() { } private void handleDeferredResume() { if (hasBeenResumed() && !getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)) { @@ -178,14 +174,6 @@ public abstract class StatefulActivity> Utilities.postAsyncCallback(mHandler, mHandleDeferredResume); } - /** - * Gives subclasses a chance to override some window insets (via - * {@link android.view.WindowInsets.Builder#setInsets(int, Insets)}). - */ - @RequiresApi(api = Build.VERSION_CODES.R) - public void updateWindowInsets(WindowInsets.Builder updatedInsetsBuilder, - WindowInsets oldInsets) { } - /** * Runs the given {@param r} runnable when this activity binds to the touch interaction service. */ diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java index 8b520165ce..4cfced8561 100644 --- a/src/com/android/launcher3/states/HintState.java +++ b/src/com/android/launcher3/states/HintState.java @@ -43,7 +43,7 @@ public class HintState extends LauncherState { } @Override - public int getTransitionDuration(Context context) { + public int getTransitionDuration(Context context, boolean isToState) { return 80; } diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java index 867fd990d0..38b62d466e 100644 --- a/src/com/android/launcher3/states/RotationHelper.java +++ b/src/com/android/launcher3/states/RotationHelper.java @@ -21,7 +21,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; import static com.android.launcher3.Utilities.dpiFromPx; -import static com.android.launcher3.util.WindowManagerCompat.MIN_TABLET_WIDTH; +import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; @@ -109,7 +109,7 @@ public class RotationHelper implements OnSharedPreferenceChangeListener, @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { - if (mDestroyed) return; + if (mDestroyed || mIgnoreAutoRotateSettings) return; boolean wasRotationEnabled = mHomeRotationEnabled; mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, getAllowRotationDefaultValue(mActivity.getDeviceProfile())); diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java index d52594e409..a205ab55ca 100644 --- a/src/com/android/launcher3/states/SpringLoadedState.java +++ b/src/com/android/launcher3/states/SpringLoadedState.java @@ -18,7 +18,6 @@ package com.android.launcher3.states; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import android.content.Context; -import android.graphics.Rect; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; @@ -40,40 +39,26 @@ public class SpringLoadedState extends LauncherState { } @Override - public int getTransitionDuration(Context context) { + public int getTransitionDuration(Context context, boolean isToState) { return 150; } @Override public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) { DeviceProfile grid = launcher.getDeviceProfile(); - Workspace ws = launcher.getWorkspace(); + Workspace ws = launcher.getWorkspace(); if (ws.getChildCount() == 0) { return super.getWorkspaceScaleAndTranslation(launcher); } - if (grid.isVerticalBarLayout()) { - float scale = grid.workspaceSpringLoadShrinkFactor; - return new ScaleAndTranslation(scale, 0, 0); - } - - float scale = grid.workspaceSpringLoadShrinkFactor; - Rect insets = launcher.getDragLayer().getInsets(); - - float scaledHeight = scale * ws.getNormalChildHeight(); - float shrunkTop = insets.top + grid.dropTargetBarSizePx; - float shrunkBottom = ws.getMeasuredHeight() - insets.bottom - - grid.workspacePadding.bottom - - grid.workspaceSpringLoadedBottomSpace; - float totalShrunkSpace = shrunkBottom - shrunkTop; - - float desiredCellTop = shrunkTop + (totalShrunkSpace - scaledHeight) / 2; + float shrunkTop = grid.getCellLayoutSpringLoadShrunkTop(); + float scale = grid.getWorkspaceSpringLoadScale(); float halfHeight = ws.getHeight() / 2; float myCenter = ws.getTop() + halfHeight; float cellTopFromCenter = halfHeight - ws.getChildAt(0).getTop(); float actualCellTop = myCenter - cellTopFromCenter * scale; - return new ScaleAndTranslation(scale, 0, (desiredCellTop - actualCellTop) / scale); + return new ScaleAndTranslation(scale, 0, (shrunkTop - actualCellTop) / scale); } @Override diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java index bd6f7d3a74..f99519d6e9 100644 --- a/src/com/android/launcher3/states/StateAnimationConfig.java +++ b/src/com/android/launcher3/states/StateAnimationConfig.java @@ -53,6 +53,7 @@ public class StateAnimationConfig { ANIM_WORKSPACE_FADE, ANIM_HOTSEAT_SCALE, ANIM_HOTSEAT_TRANSLATE, + ANIM_HOTSEAT_FADE, ANIM_OVERVIEW_SCALE, ANIM_OVERVIEW_TRANSLATE_X, ANIM_OVERVIEW_TRANSLATE_Y, @@ -62,6 +63,7 @@ public class StateAnimationConfig { ANIM_OVERVIEW_MODAL, ANIM_DEPTH, ANIM_OVERVIEW_ACTIONS_FADE, + ANIM_WORKSPACE_PAGE_TRANSLATE_X, }) @Retention(RetentionPolicy.SOURCE) public @interface AnimType {} @@ -71,6 +73,7 @@ public class StateAnimationConfig { public static final int ANIM_WORKSPACE_FADE = 3; public static final int ANIM_HOTSEAT_SCALE = 4; public static final int ANIM_HOTSEAT_TRANSLATE = 5; + public static final int ANIM_HOTSEAT_FADE = 16; public static final int ANIM_OVERVIEW_SCALE = 6; public static final int ANIM_OVERVIEW_TRANSLATE_X = 7; public static final int ANIM_OVERVIEW_TRANSLATE_Y = 8; @@ -80,8 +83,9 @@ public class StateAnimationConfig { public static final int ANIM_OVERVIEW_MODAL = 12; public static final int ANIM_DEPTH = 13; public static final int ANIM_OVERVIEW_ACTIONS_FADE = 14; + public static final int ANIM_WORKSPACE_PAGE_TRANSLATE_X = 15; - private static final int ANIM_TYPES_COUNT = 15; + private static final int ANIM_TYPES_COUNT = 17; protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT]; diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index 8ebfd62b69..242d2d423c 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -23,16 +23,23 @@ import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.Point; +import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.view.WindowInsets; +import androidx.annotation.Nullable; + +import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherState; import com.android.launcher3.R; +import com.android.launcher3.Workspace; +import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.util.ResourceBasedOverride; import com.android.launcher3.widget.picker.WidgetsFullSheet; @@ -62,12 +69,18 @@ public class TestInformationHandler implements ResourceBasedOverride { mLauncherAppState = LauncherAppState.getInstanceNoCreate(); } - public Bundle call(String method) { - return call(method, /*arg=*/ null); - } - - public Bundle call(String method, String arg) { + /** + * handle a request and return result Bundle. + * + * @param method request name. + * @param arg optional single string argument. + * @param extra extra request payload. + */ + public Bundle call(String method, String arg, @Nullable Bundle extra) { final Bundle response = new Bundle(); + if (extra != null && extra.getClassLoader() == null) { + extra.setClassLoader(getClass().getClassLoader()); + } switch (method) { case TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT: { return getLauncherUIProperty(Bundle::putInt, l -> { @@ -163,11 +176,54 @@ public class TestInformationHandler implements ResourceBasedOverride { .forceAllowRotationForTesting(Boolean.parseBoolean(arg))); return null; + case TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE: + return getLauncherUIProperty(Bundle::putIntArray, launcher -> { + final Workspace workspace = launcher.getWorkspace(); + final int screenId = workspace.getScreenIdForPageIndex( + workspace.getCurrentPage()); + final CellLayout cellLayout = workspace.getScreenWithId(screenId); + return new int[]{cellLayout.getCountX(), cellLayout.getCountY()}; + }); + + case TestProtocol.REQUEST_WORKSPACE_CELL_CENTER: + final WorkspaceCellCenterRequest request = extra.getParcelable( + TestProtocol.TEST_INFO_REQUEST_FIELD); + return getLauncherUIProperty(Bundle::putParcelable, launcher -> { + final Workspace workspace = launcher.getWorkspace(); + // TODO(b/216387249): allow caller selecting different pages. + CellLayout cellLayout = (CellLayout) workspace.getPageAt( + workspace.getCurrentPage()); + final Rect cellRect = getDescendantRectRelativeToDragLayerForCell(launcher, + cellLayout, request.cellX, request.cellY, request.spanX, request.spanY); + return new Point(cellRect.centerX(), cellRect.centerY()); + }); + + case TestProtocol.REQUEST_HAS_TIS: { + response.putBoolean( + TestProtocol.REQUEST_HAS_TIS, false); + return response; + } + default: return null; } } + private static Rect getDescendantRectRelativeToDragLayerForCell(Launcher launcher, + CellLayout cellLayout, int cellX, int cellY, int spanX, int spanY) { + final DragLayer dragLayer = launcher.getDragLayer(); + final Rect target = new Rect(); + + cellLayout.cellToRect(cellX, cellY, spanX, spanY, target); + int[] leftTop = {target.left, target.top}; + int[] rightBottom = {target.right, target.bottom}; + dragLayer.getDescendantCoordRelativeToSelf(cellLayout, leftTop); + dragLayer.getDescendantCoordRelativeToSelf(cellLayout, rightBottom); + + target.set(leftTop[0], leftTop[1], rightBottom[0], rightBottom[1]); + return target; + } + protected boolean isLauncherInitialized() { return Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null || LauncherAppState.getInstance(mContext).getModel().isModelLoaded(); diff --git a/src/com/android/launcher3/testing/TestInformationProvider.java b/src/com/android/launcher3/testing/TestInformationProvider.java index 4f2619cc84..bcc7c2de4e 100644 --- a/src/com/android/launcher3/testing/TestInformationProvider.java +++ b/src/com/android/launcher3/testing/TestInformationProvider.java @@ -60,7 +60,7 @@ public class TestInformationProvider extends ContentProvider { if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { TestInformationHandler handler = TestInformationHandler.newInstance(getContext()); handler.init(getContext()); - return handler.call(method, arg); + return handler.call(method, arg, extras); } return null; } diff --git a/src/com/android/launcher3/testing/TestInformationRequest.java b/src/com/android/launcher3/testing/TestInformationRequest.java new file mode 100644 index 0000000000..272ae56a30 --- /dev/null +++ b/src/com/android/launcher3/testing/TestInformationRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.testing; + +import android.os.Parcelable; + +/** + * A Request sent to TestInformationHandler can implement this interface to carry more information. + */ +public interface TestInformationRequest extends Parcelable { + /** + * The name for handler to dispatch request. + */ + String getRequestName(); +} diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java index 28e7553e78..3a030a8b87 100644 --- a/src/com/android/launcher3/testing/TestProtocol.java +++ b/src/com/android/launcher3/testing/TestProtocol.java @@ -68,22 +68,24 @@ public final class TestProtocol { } } + public static final String TEST_INFO_REQUEST_FIELD = "request"; public static final String TEST_INFO_RESPONSE_FIELD = "response"; public static final String REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT = "home-to-overview-swipe-height"; public static final String REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT = "background-to-overview-swipe-height"; - public static final String REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT = - "all-apps-to-overview-swipe-height"; public static final String REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT = "home-to-all-apps-swipe-height"; public static final String REQUEST_ICON_HEIGHT = "icon-height"; - public static final String REQUEST_HOTSEAT_TOP = "hotseat-top"; public static final String REQUEST_IS_LAUNCHER_INITIALIZED = "is-launcher-initialized"; public static final String REQUEST_FREEZE_APP_LIST = "freeze-app-list"; public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list"; + public static final String REQUEST_ENABLE_MANUAL_TASKBAR_STASHING = "enable-taskbar-stashing"; + public static final String REQUEST_DISABLE_MANUAL_TASKBAR_STASHING = "disable-taskbar-stashing"; + public static final String REQUEST_UNSTASH_TASKBAR_IF_STASHED = "unstash-taskbar-if-stashed"; + public static final String REQUEST_STASHED_TASKBAR_HEIGHT = "stashed-taskbar-height"; public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags"; public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y"; public static final String REQUEST_WIDGETS_SCROLL_Y = "widgets-scroll-y"; @@ -98,16 +100,26 @@ public final class TestProtocol { public static final String REQUEST_GET_HAD_NONTEST_EVENTS = "get-had-nontest-events"; public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging"; public static final String REQUEST_CLEAR_DATA = "clear-data"; + public static final String REQUEST_USE_TEST_WORKSPACE_LAYOUT = "use-test-workspace-layout"; + public static final String REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT = + "use-default-workspace-layout"; + public static final String REQUEST_HOTSEAT_ICON_NAMES = "get-hotseat-icon-names"; public static final String REQUEST_IS_TABLET = "is-tablet"; public static final String REQUEST_IS_TWO_PANELS = "is-two-panel"; public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold"; public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT = "get-activities-created-count"; public static final String REQUEST_GET_ACTIVITIES = "get-activities"; + public static final String REQUEST_HAS_TIS = "has-touch-interaction-service"; + + public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size"; + public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center"; + public static final String REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET = "get-focused-task-height-for-tablet"; public static final String REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET = "get-grid-task-size-rect-for-tablet"; + public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing"; public static final String REQUEST_ENABLE_ROTATION = "enable_rotation"; public static Long sForcePauseTimeout; @@ -122,9 +134,9 @@ public final class TestProtocol { public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation"; public static final String PERMANENT_DIAG_TAG = "TaplTarget"; - public static final String TASK_VIEW_ID_CRASH = "b/195430732"; public static final String NO_DROP_TARGET = "b/195031154"; public static final String NULL_INT_SET = "b/200572078"; - + public static final String MISSING_PROMISE_ICON = "b/202985412"; public static final String BAD_STATE = "b/223498680"; + public static final String TASKBAR_IN_APP_STATE = "b/227657604"; } diff --git a/src/com/android/launcher3/testing/WorkspaceCellCenterRequest.java b/src/com/android/launcher3/testing/WorkspaceCellCenterRequest.java new file mode 100644 index 0000000000..71ab09f3f2 --- /dev/null +++ b/src/com/android/launcher3/testing/WorkspaceCellCenterRequest.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.testing; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Request object for querying a workspace cell region in Rect. + */ +public class WorkspaceCellCenterRequest implements TestInformationRequest { + public final int cellX; + public final int cellY; + public final int spanX; + public final int spanY; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(cellX); + dest.writeInt(cellY); + dest.writeInt(spanX); + dest.writeInt(spanY); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public WorkspaceCellCenterRequest createFromParcel(Parcel source) { + return new WorkspaceCellCenterRequest(source); + } + + @Override + public WorkspaceCellCenterRequest[] newArray(int size) { + return new WorkspaceCellCenterRequest[size]; + } + }; + + private WorkspaceCellCenterRequest(int cellX, int cellY, int spanX, int spanY) { + this.cellX = cellX; + this.cellY = cellY; + this.spanX = spanX; + this.spanY = spanY; + } + + private WorkspaceCellCenterRequest(Parcel in) { + this(in.readInt(), in.readInt(), in.readInt(), in.readInt()); + } + + /** + * Create a builder for WorkspaceCellRectRequest. + * + * @return WorkspaceCellRectRequest builder. + */ + public static WorkspaceCellCenterRequest.Builder builder() { + return new WorkspaceCellCenterRequest.Builder(); + } + + @Override + public String getRequestName() { + return TestProtocol.REQUEST_WORKSPACE_CELL_CENTER; + } + + /** + * WorkspaceCellRectRequest Builder. + */ + public static final class Builder { + private int mCellX; + private int mCellY; + private int mSpanX; + private int mSpanY; + + private Builder() { + this.mCellX = 0; + this.mCellY = 0; + this.mSpanX = 1; + this.mSpanY = 1; + } + + /** + * Set X coordinate of upper left corner expressed as a cell position + */ + public WorkspaceCellCenterRequest.Builder setCellX(int x) { + this.mCellX = x; + return this; + } + + /** + * Set Y coordinate of upper left corner expressed as a cell position + */ + public WorkspaceCellCenterRequest.Builder setCellY(int y) { + this.mCellY = y; + return this; + } + + /** + * Set span Width in cells + */ + public WorkspaceCellCenterRequest.Builder setSpanX(int x) { + this.mSpanX = x; + return this; + } + + /** + * Set span Height in cells + */ + public WorkspaceCellCenterRequest.Builder setSpanY(int y) { + this.mCellY = y; + return this; + } + + /** + * build the WorkspaceCellRectRequest. + */ + public WorkspaceCellCenterRequest build() { + return new WorkspaceCellCenterRequest(mCellX, mCellY, mSpanX, mSpanY); + } + } +} diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index 61d488c91b..09b8228182 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -16,17 +16,19 @@ package com.android.launcher3.touch; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import android.animation.Animator.AnimatorListener; import android.animation.ValueAnimator; @@ -285,8 +287,13 @@ public abstract class AbstractStateChangeTouchController ? mToState : mFromState; // snap to top or bottom using the release velocity } else { + float successTransitionProgress = + mLauncher.getDeviceProfile().isTablet + && (mToState == ALL_APPS || mFromState == ALL_APPS) + ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS + : SUCCESS_TRANSITION_PROGRESS; targetState = - (interpolatedProgress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState; + (interpolatedProgress > successTransitionProgress) ? mToState : mFromState; } final float endProgress; @@ -324,9 +331,6 @@ public abstract class AbstractStateChangeTouchController Math.min(progress, 1) - endProgress) * durationMultiplier; } } - if (targetState != mStartState) { - logReachedState(targetState); - } mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState)); ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); anim.setFloatValues(startProgress, endProgress); @@ -355,6 +359,8 @@ public abstract class AbstractStateChangeTouchController boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState); if (shouldGoToTargetState) { goToTargetState(targetState); + } else { + logReachedState(mToState); } } @@ -362,13 +368,19 @@ public abstract class AbstractStateChangeTouchController if (!mLauncher.isInState(targetState)) { // If we're already in the target state, don't jump to it at the end of the animation in // case the user started interacting with it before the animation finished. - mLauncher.getStateManager().goToState(targetState, false /* animated */); + mLauncher.getStateManager().goToState(targetState, false /* animated */, + forEndCallback(() -> logReachedState(targetState))); + } else { + logReachedState(targetState); } mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim( 1f).setDuration(0).start(); } private void logReachedState(LauncherState targetState) { + if (mStartState == targetState) { + return; + } // Transition complete. log the action mLauncher.getStatsLogManager().logger() .withSrcState(mStartState.statsLogOrdinal) diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java index 989a9e4c24..db43baa4d8 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -17,9 +17,21 @@ package com.android.launcher3.touch; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.anim.Interpolators.DECELERATED_EASE; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED_ACCELERATE; +import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE; +import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; +import static com.android.launcher3.anim.Interpolators.INSTANT; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE; import android.view.MotionEvent; import android.view.animation.Interpolator; @@ -37,11 +49,47 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { private static final float ALLAPPS_STAGGERED_FADE_THRESHOLD = 0.5f; + // Custom timing for NORMAL -> ALL_APPS on phones only. + private static final float WORKSPACE_MOTION_START = 0.1667f; + private static final float ALL_APPS_STATE_TRANSITION = 0.305f; + private static final float ALL_APPS_FADE_END = 0.4717f; + private static final float ALL_APPS_FULL_DEPTH_PROGRESS = 0.5f; + public static final Interpolator ALLAPPS_STAGGERED_FADE_EARLY_RESPONDER = Interpolators.clampToProgress(LINEAR, 0, ALLAPPS_STAGGERED_FADE_THRESHOLD); public static final Interpolator ALLAPPS_STAGGERED_FADE_LATE_RESPONDER = Interpolators.clampToProgress(LINEAR, ALLAPPS_STAGGERED_FADE_THRESHOLD, 1f); + // Custom interpolators for NORMAL -> ALL_APPS on phones only. + // The blur to All Apps is set to be complete when the interpolator is at 0.5. + public static final Interpolator BLUR = + Interpolators.clampToProgress( + Interpolators.mapToProgress( + LINEAR, 0f, ALL_APPS_FULL_DEPTH_PROGRESS), + WORKSPACE_MOTION_START, ALL_APPS_STATE_TRANSITION); + public static final Interpolator WORKSPACE_FADE = + Interpolators.clampToProgress(FINAL_FRAME, 0f, ALL_APPS_STATE_TRANSITION); + public static final Interpolator WORKSPACE_SCALE = + Interpolators.clampToProgress( + EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START, ALL_APPS_STATE_TRANSITION); + public static final Interpolator HOTSEAT_FADE = WORKSPACE_FADE; + public static final Interpolator HOTSEAT_SCALE = HOTSEAT_FADE; + public static final Interpolator HOTSEAT_TRANSLATE = + Interpolators.clampToProgress( + EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START, ALL_APPS_STATE_TRANSITION); + public static final Interpolator SCRIM_FADE = + Interpolators.clampToProgress( + Interpolators.mapToProgress(LINEAR, 0f, 0.8f), + WORKSPACE_MOTION_START, ALL_APPS_STATE_TRANSITION); + public static final Interpolator ALL_APPS_FADE = + Interpolators.clampToProgress( + Interpolators.mapToProgress(DECELERATED_EASE, 0.2f, 1.0f), + ALL_APPS_STATE_TRANSITION, ALL_APPS_FADE_END); + public static final Interpolator ALL_APPS_VERTICAL_PROGRESS = + Interpolators.clampToProgress( + Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1.0f), + ALL_APPS_STATE_TRANSITION, 1.0f); + public AllAppsSwipeController(Launcher l) { super(l, SingleAxisSwipeDetector.VERTICAL); } @@ -94,9 +142,9 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { LauncherState toState) { StateAnimationConfig config = super.getConfigForStates(fromState, toState); if (fromState == NORMAL && toState == ALL_APPS) { - applyNormalToAllAppsAnimConfig(config); + applyNormalToAllAppsAnimConfig(mLauncher, config); } else if (fromState == ALL_APPS && toState == NORMAL) { - applyAllAppsToNormalConfig(config); + applyAllAppsToNormalConfig(mLauncher, config); } return config; } @@ -104,18 +152,34 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { /** * Applies Animation config values for transition from all apps to home */ - public static void applyAllAppsToNormalConfig(StateAnimationConfig config) { + public static void applyAllAppsToNormalConfig(Launcher launcher, StateAnimationConfig config) { + boolean isTablet = launcher.getDeviceProfile().isTablet; config.setInterpolator(ANIM_SCRIM_FADE, ALLAPPS_STAGGERED_FADE_LATE_RESPONDER); - config.setInterpolator(ANIM_ALL_APPS_FADE, ALLAPPS_STAGGERED_FADE_EARLY_RESPONDER); + config.setInterpolator(ANIM_ALL_APPS_FADE, isTablet + ? FINAL_FRAME : ALLAPPS_STAGGERED_FADE_EARLY_RESPONDER); + if (!isTablet) { + config.setInterpolator(ANIM_WORKSPACE_FADE, INSTANT); + } } /** * Applies Animation config values for transition from home to all apps */ - public static void applyNormalToAllAppsAnimConfig(StateAnimationConfig config) { - config.setInterpolator(ANIM_SCRIM_FADE, ALLAPPS_STAGGERED_FADE_EARLY_RESPONDER); - config.setInterpolator(ANIM_ALL_APPS_FADE, ALLAPPS_STAGGERED_FADE_LATE_RESPONDER); + public static void applyNormalToAllAppsAnimConfig(Launcher launcher, + StateAnimationConfig config) { + if (launcher.getDeviceProfile().isTablet) { + config.setInterpolator(ANIM_SCRIM_FADE, ALLAPPS_STAGGERED_FADE_EARLY_RESPONDER); + config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT); + } else { + config.setInterpolator(ANIM_DEPTH, BLUR); + config.setInterpolator(ANIM_WORKSPACE_FADE, WORKSPACE_FADE); + config.setInterpolator(ANIM_WORKSPACE_SCALE, WORKSPACE_SCALE); + config.setInterpolator(ANIM_HOTSEAT_FADE, HOTSEAT_FADE); + config.setInterpolator(ANIM_HOTSEAT_SCALE, HOTSEAT_SCALE); + config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, HOTSEAT_TRANSLATE); + config.setInterpolator(ANIM_SCRIM_FADE, SCRIM_FADE); + config.setInterpolator(ANIM_ALL_APPS_FADE, ALL_APPS_FADE); + config.setInterpolator(ANIM_VERTICAL_PROGRESS, ALL_APPS_VERTICAL_PROGRESS); + } } - - } diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index 8d57d695fa..e95a787cfd 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -43,6 +43,7 @@ import android.widget.Toast; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.folder.Folder; @@ -58,8 +59,10 @@ import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.SearchActionItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; +import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.views.FloatingIconView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; @@ -67,6 +70,8 @@ import com.android.launcher3.widget.PendingAppWidgetHostView; import com.android.launcher3.widget.WidgetAddFlowHandler; import com.android.launcher3.widget.WidgetManagerHelper; +import java.util.Collections; + /** * Class for handling clicks on workspace and all-apps items */ @@ -95,8 +100,7 @@ public class ItemClickHandler { onClickFolderIcon(v); } } else if (tag instanceof AppInfo) { - startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher - ); + startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher); } else if (tag instanceof LauncherAppWidgetInfo) { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v, launcher); @@ -171,7 +175,9 @@ public class ItemClickHandler { (d, i) -> startMarketIntentForPackage(v, launcher, packageName)) .setNeutralButton(R.string.abandoned_clean_this, (d, i) -> launcher.getWorkspace() - .removeAbandonedPromise(packageName, user)) + .persistRemoveItemsByMatcher(ItemInfoMatcher.ofPackages( + Collections.singleton(packageName), user), + "user explicitly removes the promise app icon")) .create().show(); } @@ -205,6 +211,12 @@ public class ItemClickHandler { public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) { final int disabledFlags = shortcut.runtimeStatusFlags & WorkspaceItemInfo.FLAG_DISABLED_MASK; + // Handle the case where the disabled reason is DISABLED_REASON_VERSION_LOWER. + // Show an AlertDialog for the user to choose either updating the app or cancel the launch. + if (maybeCreateAlertDialogForShortcut(shortcut, context)) { + return true; + } + if ((disabledFlags & ~FLAG_DISABLED_SUSPENDED & ~FLAG_DISABLED_QUIET_USER) == 0) { @@ -230,6 +242,38 @@ public class ItemClickHandler { } } + private static boolean maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut, + Context context) { + try { + final Launcher launcher = Launcher.getLauncher(context); + if (shortcut.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT + && shortcut.isDisabledVersionLower()) { + + new AlertDialog.Builder(context) + .setTitle(R.string.dialog_update_title) + .setMessage(R.string.dialog_update_message) + .setPositiveButton(R.string.dialog_update, (d, i) -> { + // Direct the user to the play store to update the app + context.startActivity(shortcut.getMarketIntent(context)); + }) + .setNeutralButton(R.string.dialog_remove, (d, i) -> { + // Remove the icon if launcher is successfully initialized + launcher.getWorkspace().persistRemoveItemsByMatcher(ItemInfoMatcher + .ofShortcutKeys(Collections.singleton(ShortcutKey + .fromItemInfo(shortcut))), + "user explicitly removes disabled shortcut"); + }) + .create() + .show(); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error creating alert dialog", e); + } + + return false; + } + /** * Event handler for an app shortcut click. * diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java index f876dd9230..6bae745ef8 100644 --- a/src/com/android/launcher3/touch/ItemLongClickListener.java +++ b/src/com/android/launcher3/touch/ItemLongClickListener.java @@ -37,6 +37,7 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.views.BubbleTextHolder; /** * Class to handle long-clicks on workspace items and start drag as a result. @@ -79,9 +80,12 @@ public class ItemLongClickListener { launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions); } - private static boolean onAllAppsItemLongClick(View v) { + private static boolean onAllAppsItemLongClick(View view) { TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onAllAppsItemLongClick"); - v.cancelLongPress(); + view.cancelLongPress(); + View v = (view instanceof BubbleTextHolder) + ? ((BubbleTextHolder) view).getBubbleText() + : view; Launcher launcher = Launcher.getLauncher(v.getContext()); if (!canStartDrag(launcher)) return false; // When we have exited all apps or are in transition, disregard long clicks diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java index a94ad7c2bb..b477905dff 100644 --- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java +++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java @@ -16,6 +16,7 @@ package com.android.launcher3.touch; +import static android.view.Gravity.BOTTOM; import static android.view.Gravity.CENTER_VERTICAL; import static android.view.Gravity.END; import static android.view.Gravity.START; @@ -101,6 +102,17 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { velocity.set(-oldY, oldX); } + @Override + public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) { + // We don't need to check the "top" value here because the startRect is in the orientation + // of the app, not of the fixed portrait launcher. + if (outStartRect.left > deviceProfile.heightPx) { + outStartRect.offsetTo(0, outStartRect.top); + } else if (outStartRect.left < -deviceProfile.heightPx) { + outStartRect.offsetTo(0, outStartRect.top); + } + } + @Override public void setPrimary(T target, Int2DAction action, int param) { action.call(target, 0, param); @@ -177,18 +189,6 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { return VIEW_TRANSLATE_X; } - @Override - public int getSplitTaskViewDismissDirection(@StagePosition int stagePosition, - DeviceProfile dp) { - // Don't use device profile here because we know we're in fake landscape, only split option - // available is top/left - if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) { - // Top (visually left) side - return SPLIT_TRANSLATE_PRIMARY_NEGATIVE; - } - throw new IllegalStateException("Invalid split stage position: " + stagePosition); - } - @Override public int getPrimaryScroll(View view) { return view.getScrollY(); @@ -310,9 +310,10 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { } @Override - public Pair setDwbLayoutParamsAndGetTranslations(int taskViewWidth, + public Pair getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner) { + boolean isRtl = banner.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; float translationX = 0; float translationY = 0; FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); @@ -323,7 +324,7 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { FrameLayout.LayoutParams snapshotParams = (FrameLayout.LayoutParams) thumbnailViews[0] .getLayoutParams(); - bannerParams.gravity = TOP | START; + bannerParams.gravity = TOP | (isRtl ? END : START); if (splitBounds == null) { // Single, fullscreen case bannerParams.width = taskViewHeight - snapshotParams.topMargin; @@ -339,9 +340,11 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { // Set translations if (desiredTaskId == splitBounds.rightBottomTaskId) { - translationY = (snapshotParams.topMargin + taskViewHeight) - * (splitBounds.leftTaskPercent) + - (taskViewHeight * splitBounds.dividerWidthPercent); + float topLeftTaskPlusDividerPercent = splitBounds.appsStackedVertically + ? (splitBounds.topTaskPercent + splitBounds.dividerHeightPercent) + : (splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent); + translationY = snapshotParams.topMargin + + ((taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent); } if (desiredTaskId == splitBounds.leftTopTaskId) { translationY = snapshotParams.topMargin; @@ -402,12 +405,34 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { } @Override - public void getInitialSplitPlaceholderBounds(int placeholderHeight, DeviceProfile dp, - @StagePosition int stagePosition, Rect out) { + public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, + DeviceProfile dp, @StagePosition int stagePosition, Rect out) { // In fake land/seascape, the placeholder always needs to go to the "top" of the device, // which is the same bounds as 0 rotation. int width = dp.widthPx; - out.set(0, 0, width, placeholderHeight); + int insetThickness = dp.getInsets().top; + out.set(0, 0, width, placeholderHeight + insetThickness); + out.inset(placeholderInset, 0); + + // Adjust the top to account for content off screen. This will help to animate the view in + // with rounded corners. + int screenWidth = dp.widthPx; + int screenHeight = dp.heightPx; + int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) + / screenWidth); + out.top -= (totalHeight - placeholderHeight); + } + + @Override + public void updateStagedSplitIconParams(View out, float onScreenRectCenterX, + float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, + int drawableWidth, int drawableHeight, DeviceProfile dp, + @StagePosition int stagePosition) { + float inset = dp.getInsets().top; + out.setX(Math.round(onScreenRectCenterX / fullscreenScaleX + - 1.0f * drawableWidth / 2)); + out.setY(Math.round((onScreenRectCenterY + (inset / 2f)) / fullscreenScaleY + - 1.0f * drawableHeight / 2)); } @Override @@ -423,24 +448,29 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { @Override public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, StagedSplitBounds splitInfo, int desiredStagePosition) { - float diff; - float horizontalDividerDiff = splitInfo.visualDividerBounds.width() / 2f; + float topLeftTaskPercent = splitInfo.appsStackedVertically + ? splitInfo.topTaskPercent + : splitInfo.leftTaskPercent; + float dividerBarPercent = splitInfo.appsStackedVertically + ? splitInfo.dividerHeightPercent + : splitInfo.dividerWidthPercent; + if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { - diff = outRect.height() * (1f - splitInfo.leftTaskPercent) + horizontalDividerDiff; - outRect.bottom -= diff; + outRect.bottom = outRect.top + (int) (outRect.height() * topLeftTaskPercent); } else { - diff = outRect.height() * splitInfo.leftTaskPercent + horizontalDividerDiff; - outRect.top += diff; + outRect.top += (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent)); } } @Override public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, - int parentWidth, int parentHeight, - StagedSplitBounds splitBoundsConfig, DeviceProfile dp) { + int parentWidth, int parentHeight, StagedSplitBounds splitBoundsConfig, + DeviceProfile dp, boolean isRtl) { int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; - int dividerBar = splitBoundsConfig.visualDividerBounds.width(); + int dividerBar = splitBoundsConfig.appsStackedVertically + ? splitBoundsConfig.visualDividerBounds.height() + : splitBoundsConfig.visualDividerBounds.width(); int primarySnapshotHeight; int primarySnapshotWidth; int secondarySnapshotHeight; @@ -464,35 +494,54 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler { } @Override - public void setIconAndSnapshotParams(View iconView, int taskIconMargin, int taskIconHeight, - FrameLayout.LayoutParams snapshotParams, boolean isRtl) { - FrameLayout.LayoutParams iconParams = - (FrameLayout.LayoutParams) iconView.getLayoutParams(); + public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, + int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL; iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2; iconParams.leftMargin = 0; - iconParams.topMargin = snapshotParams.topMargin / 2; + iconParams.topMargin = thumbnailTopMargin / 2; } @Override public void setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - boolean isRtl, DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { + int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, + DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { FrameLayout.LayoutParams primaryIconParams = (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); FrameLayout.LayoutParams secondaryIconParams = new FrameLayout.LayoutParams(primaryIconParams); - int dividerBar = (splitConfig.appsStackedVertically ? - splitConfig.visualDividerBounds.height() : - splitConfig.visualDividerBounds.width()); - primaryIconParams.gravity = (isRtl ? START : END) | TOP; - primaryIconView.setTranslationY(primarySnapshotHeight - primaryIconView.getHeight() / 2f); + // We calculate the "midpoint" of the thumbnail area, and place the icons there. + // This is the place where the thumbnail area splits by default, in a near-50/50 split. + // It is usually not exactly 50/50, due to insets/screen cutouts. + int fullscreenInsetThickness = deviceProfile.getInsets().top; + int fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness) + / 2); + float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx; + float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx; + int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx; + int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots; + int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct); + int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); + + primaryIconParams.gravity = BOTTOM | (isRtl ? START : END); + secondaryIconParams.gravity = BOTTOM | (isRtl ? START : END); primaryIconView.setTranslationX(0); - - secondaryIconParams.gravity = (isRtl ? START : END) | TOP; - secondaryIconView.setTranslationY(primarySnapshotHeight + taskIconHeight + dividerBar); secondaryIconView.setTranslationX(0); + if (splitConfig.initiatedFromSeascape) { + // if the split was initiated from seascape, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset); + secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset + + taskIconHeight); + } else { + // if not, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationY(-bottomToMidpointOffset); + secondaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight); + } + primaryIconView.setLayoutParams(primaryIconParams); secondaryIconView.setLayoutParams(secondaryIconParams); } diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java index 19c4639f1d..ca46aa8a84 100644 --- a/src/com/android/launcher3/touch/PagedOrientationHandler.java +++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java @@ -47,10 +47,6 @@ import java.util.List; */ public interface PagedOrientationHandler { - int SPLIT_TRANSLATE_PRIMARY_POSITIVE = 0; - int SPLIT_TRANSLATE_PRIMARY_NEGATIVE = 1; - int SPLIT_TRANSLATE_SECONDARY_NEGATIVE = 2; - PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler(); PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler(); PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler(); @@ -82,12 +78,6 @@ public interface PagedOrientationHandler { FloatProperty getPrimaryViewTranslate(); FloatProperty getSecondaryViewTranslate(); - /** - * @param stagePosition The position where the view to be split will go - * @return {@link #SPLIT_TRANSLATE_*} constants to indicate which direction the - * dismissal should happen - */ - int getSplitTaskViewDismissDirection(@StagePosition int stagePosition, DeviceProfile dp); int getPrimaryScroll(View view); float getPrimaryScale(View view); int getChildStart(View view); @@ -120,10 +110,29 @@ public interface PagedOrientationHandler { int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect); List getSplitPositionOptions(DeviceProfile dp); /** - * @param splitholderSize height of placeholder view in portrait, width in landscape + * @param placeholderHeight height of placeholder view in portrait, width in landscape */ - void getInitialSplitPlaceholderBounds(int splitholderSize, DeviceProfile dp, - @StagePosition int stagePosition, Rect out); + void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, + DeviceProfile dp, @StagePosition int stagePosition, Rect out); + + /** + * Centers an icon in the split staging area, accounting for insets. + * @param out The icon that needs to be centered. + * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is + * offscreen). + * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is + * offscreen). + * @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels. + * @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels. + * @param drawableWidth The icon's drawable (final) width. + * @param drawableHeight The icon's drawable (final) height. + * @param dp The device profile, used to report rotation and hardware insets. + * @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right. + */ + void updateStagedSplitIconParams(View out, float onScreenRectCenterX, + float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, + int drawableWidth, int drawableHeight, DeviceProfile dp, + @StagePosition int stagePosition); /** * @param splitDividerSize height of split screen drag handle in portrait, width in landscape @@ -149,14 +158,15 @@ public interface PagedOrientationHandler { void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, int parentWidth, int parentHeight, - StagedSplitBounds splitBoundsConfig, DeviceProfile dp); + StagedSplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl); // Overview TaskMenuView methods - void setIconAndSnapshotParams(View iconView, int taskIconMargin, int taskIconHeight, - FrameLayout.LayoutParams snapshotParams, boolean isRtl); + void setTaskIconParams(FrameLayout.LayoutParams iconParams, + int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl); void setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - boolean isRtl, DeviceProfile deviceProfile, StagedSplitBounds splitConfig); + int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, + DeviceProfile deviceProfile, StagedSplitBounds splitConfig); /* * The following two methods try to center the TaskMenuView in landscape by finding the center @@ -191,7 +201,12 @@ public interface PagedOrientationHandler { */ PointF getAdditionalInsetForTaskMenu(float margin); - Pair setDwbLayoutParamsAndGetTranslations(int taskViewWidth, + /** + * Calculates the position where a Digital Wellbeing Banner should be placed on its parent + * TaskView. + * @return A Pair of Floats representing the proper x and y translations. + */ + Pair getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner); @@ -211,6 +226,12 @@ public interface PagedOrientationHandler { */ void adjustFloatingIconStartVelocity(PointF velocity); + /** + * Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries + * @param outStartRect The start rect that will directly be modified + */ + void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile); + class ChildBounds { public final int primaryDimension; diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java index ad9f95c256..508823c4d0 100644 --- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java +++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java @@ -18,6 +18,7 @@ package com.android.launcher3.touch; import static android.view.Gravity.BOTTOM; import static android.view.Gravity.CENTER_HORIZONTAL; +import static android.view.Gravity.END; import static android.view.Gravity.START; import static android.view.Gravity.TOP; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; @@ -28,7 +29,6 @@ import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; import android.content.res.Resources; import android.graphics.Matrix; @@ -47,7 +47,6 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; @@ -55,7 +54,6 @@ import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds; import com.android.launcher3.views.BaseDragLayer; -import java.util.ArrayList; import java.util.List; public class PortraitPagedViewHandler implements PagedOrientationHandler { @@ -103,6 +101,15 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { //no-op } + @Override + public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) { + if (outStartRect.left > deviceProfile.widthPx) { + outStartRect.offsetTo(0, outStartRect.top); + } else if (outStartRect.left < -deviceProfile.widthPx) { + outStartRect.offsetTo(0, outStartRect.top); + } + } + @Override public void setPrimary(T target, Int2DAction action, int param) { action.call(target, param, 0); @@ -179,24 +186,6 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { return VIEW_TRANSLATE_Y; } - @Override - public int getSplitTaskViewDismissDirection(@StagePosition int stagePosition, - DeviceProfile dp) { - if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) { - if (dp.isLandscape) { - // Left side - return SPLIT_TRANSLATE_PRIMARY_NEGATIVE; - } else { - // Top side - return SPLIT_TRANSLATE_SECONDARY_NEGATIVE; - } - } else if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { - // We don't have a bottom option, so should be right - return SPLIT_TRANSLATE_PRIMARY_POSITIVE; - } - throw new IllegalStateException("Invalid split stage position: " + stagePosition); - } - @Override public int getPrimaryScroll(View view) { return view.getScrollX(); @@ -289,9 +278,9 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { @Override public int getTaskMenuWidth(View view, DeviceProfile deviceProfile) { - return deviceProfile.isLandscape && !deviceProfile.overviewShowAsGrid ? - view.getMeasuredHeight() : - view.getMeasuredWidth(); + return deviceProfile.isLandscape && !deviceProfile.isTablet + ? view.getMeasuredHeight() + : view.getMeasuredWidth(); } @Override @@ -324,7 +313,7 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { } @Override - public Pair setDwbLayoutParamsAndGetTranslations(int taskViewWidth, + public Pair getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner) { float translationX = 0; @@ -352,16 +341,25 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { // Set translations if (deviceProfile.isLandscape) { if (desiredTaskId == splitBounds.rightBottomTaskId) { - translationX = ((taskViewWidth * splitBounds.leftTaskPercent) - + (taskViewWidth * splitBounds.dividerWidthPercent)); + float leftTopTaskPercent = splitBounds.appsStackedVertically + ? splitBounds.topTaskPercent + : splitBounds.leftTaskPercent; + float dividerThicknessPercent = splitBounds.appsStackedVertically + ? splitBounds.dividerHeightPercent + : splitBounds.dividerWidthPercent; + translationX = ((taskViewWidth * leftTopTaskPercent) + + (taskViewWidth * dividerThicknessPercent)); } } else { if (desiredTaskId == splitBounds.leftTopTaskId) { FrameLayout.LayoutParams snapshotParams = (FrameLayout.LayoutParams) thumbnailViews[0] .getLayoutParams(); + float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically + ? (1f - splitBounds.topTaskPercent) + : (1f - splitBounds.leftTaskPercent); translationY = -((taskViewHeight - snapshotParams.topMargin) - * (1f - splitBounds.topTaskPercent)); + * bottomRightTaskPlusDividerPercent); } } return new Pair<>(translationX, translationY); @@ -414,59 +412,85 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { @Override public List getSplitPositionOptions(DeviceProfile dp) { - List options = new ArrayList<>(1); - // Add both left and right options if we're in tablet mode - if (dp.isTablet && dp.isLandscape) { - options.add(new SplitPositionOption( - R.drawable.ic_split_right, R.string.split_screen_position_right, - STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); - options.add(new SplitPositionOption( - R.drawable.ic_split_left, R.string.split_screen_position_left, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); - } else { - if (dp.isSeascape()) { - // Add left/right options - options.add(new SplitPositionOption( - R.drawable.ic_split_right, R.string.split_screen_position_right, - STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); - } else if (dp.isLandscape) { - options.add(new SplitPositionOption( - R.drawable.ic_split_left, R.string.split_screen_position_left, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); - } else { - // Only add top option - options.add(new SplitPositionOption( - R.drawable.ic_split_top, R.string.split_screen_position_top, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); - } - } - return options; + return Utilities.getSplitPositionOptions(dp); } @Override - public void getInitialSplitPlaceholderBounds(int placeholderHeight, DeviceProfile dp, - @StagePosition int stagePosition, Rect out) { - int width = dp.widthPx; - out.set(0, 0, width, placeholderHeight); + public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, + DeviceProfile dp, @StagePosition int stagePosition, Rect out) { + int screenWidth = dp.widthPx; + int screenHeight = dp.heightPx; + boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; + int insetThickness; + if (!dp.isLandscape) { + insetThickness = dp.getInsets().top; + } else { + insetThickness = pinToRight ? dp.getInsets().right : dp.getInsets().left; + } + out.set(0, 0, screenWidth, placeholderHeight + insetThickness); if (!dp.isLandscape) { // portrait, phone or tablet - spans width of screen, nothing else to do + out.inset(placeholderInset, 0); + + // Adjust the top to account for content off screen. This will help to animate the view + // in with rounded corners. + int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) + / screenWidth); + out.top -= (totalHeight - placeholderHeight); return; } // Now we rotate the portrait rect depending on what side we want pinned - boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; - int screenHeight = dp.heightPx; - float postRotateScale = (float) screenHeight / width; + float postRotateScale = (float) screenHeight / screenWidth; mTmpMatrix.reset(); mTmpMatrix.postRotate(pinToRight ? 90 : 270); - mTmpMatrix.postTranslate(pinToRight ? width : 0, pinToRight ? 0 : width); + mTmpMatrix.postTranslate(pinToRight ? screenWidth : 0, pinToRight ? 0 : screenWidth); // The placeholder height stays constant after rotation, so we don't change width scale mTmpMatrix.postScale(1, postRotateScale); mTmpRectF.set(out); mTmpMatrix.mapRect(mTmpRectF); + mTmpRectF.inset(0, placeholderInset); mTmpRectF.roundOut(out); + + // Adjust the top to account for content off screen. This will help to animate the view in + // with rounded corners. + int totalWidth = (int) (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset) + / screenHeight); + int width = out.width(); + if (pinToRight) { + out.right += totalWidth - width; + } else { + out.left -= totalWidth - width; + } + } + + @Override + public void updateStagedSplitIconParams(View out, float onScreenRectCenterX, + float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, + int drawableWidth, int drawableHeight, DeviceProfile dp, + @StagePosition int stagePosition) { + boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; + if (!dp.isLandscape) { + float inset = dp.getInsets().top; + out.setX(Math.round(onScreenRectCenterX / fullscreenScaleX + - 1.0f * drawableWidth / 2)); + out.setY(Math.round((onScreenRectCenterY + (inset / 2f)) / fullscreenScaleY + - 1.0f * drawableHeight / 2)); + } else { + if (pinToRight) { + float inset = dp.getInsets().right; + out.setX(Math.round((onScreenRectCenterX - (inset / 2f)) / fullscreenScaleX + - 1.0f * drawableWidth / 2)); + } else { + float inset = dp.getInsets().left; + out.setX(Math.round((onScreenRectCenterX + (inset / 2f)) / fullscreenScaleX + - 1.0f * drawableWidth / 2)); + } + out.setY(Math.round(onScreenRectCenterY / fullscreenScaleY + - 1.0f * drawableHeight / 2)); + } } @Override @@ -503,27 +527,32 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, StagedSplitBounds splitInfo, int desiredStagePosition) { boolean isLandscape = dp.isLandscape; + float topLeftTaskPercent = splitInfo.appsStackedVertically + ? splitInfo.topTaskPercent + : splitInfo.leftTaskPercent; + float dividerBarPercent = splitInfo.appsStackedVertically + ? splitInfo.dividerHeightPercent + : splitInfo.dividerWidthPercent; + if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { if (isLandscape) { - outRect.right = outRect.left + (int) (outRect.width() * splitInfo.leftTaskPercent); + outRect.right = outRect.left + (int) (outRect.width() * topLeftTaskPercent); } else { - outRect.bottom = outRect.top + (int) (outRect.height() * splitInfo.topTaskPercent); + outRect.bottom = outRect.top + (int) (outRect.height() * topLeftTaskPercent); } } else { if (isLandscape) { - outRect.left += (int) (outRect.width() * - (splitInfo.leftTaskPercent + splitInfo.dividerWidthPercent)); + outRect.left += (int) (outRect.width() * (topLeftTaskPercent + dividerBarPercent)); } else { - outRect.top += (int) (outRect.height() * - (splitInfo.topTaskPercent + splitInfo.dividerHeightPercent)); + outRect.top += (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent)); } } } @Override public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, - int parentWidth, int parentHeight, - StagedSplitBounds splitBoundsConfig, DeviceProfile dp) { + int parentWidth, int parentHeight, StagedSplitBounds splitBoundsConfig, + DeviceProfile dp, boolean isRtl) { int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; int dividerBar = splitBoundsConfig.appsStackedVertically @@ -542,7 +571,13 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { secondarySnapshotHeight = totalThumbnailHeight; secondarySnapshotWidth = parentWidth - primarySnapshotWidth - dividerBar; int translationX = primarySnapshotWidth + dividerBar; - secondarySnapshot.setTranslationX(translationX); + if (isRtl) { + primarySnapshot.setTranslationX(-translationX); + secondarySnapshot.setTranslationX(0); + } else { + secondarySnapshot.setTranslationX(translationX); + primarySnapshot.setTranslationX(0); + } secondarySnapshot.setTranslationY(spaceAboveSnapshot); } else { primarySnapshotWidth = parentWidth; @@ -553,6 +588,7 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { int translationY = primarySnapshotHeight + spaceAboveSnapshot + dividerBar; secondarySnapshot.setTranslationY(translationY); secondarySnapshot.setTranslationX(0); + primarySnapshot.setTranslationX(0); } primarySnapshot.measure( View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY), @@ -564,10 +600,8 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { } @Override - public void setIconAndSnapshotParams(View iconView, int taskIconMargin, int taskIconHeight, - FrameLayout.LayoutParams snapshotParams, boolean isRtl) { - FrameLayout.LayoutParams iconParams = - (FrameLayout.LayoutParams) iconView.getLayoutParams(); + public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, + int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { iconParams.gravity = TOP | CENTER_HORIZONTAL; iconParams.leftMargin = iconParams.rightMargin = 0; iconParams.topMargin = taskIconMargin; @@ -576,31 +610,72 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler { @Override public void setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - boolean isRtl, DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { + int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, + DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { FrameLayout.LayoutParams primaryIconParams = (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); FrameLayout.LayoutParams secondaryIconParams = new FrameLayout.LayoutParams(primaryIconParams); - int dividerBar = (splitConfig.appsStackedVertically ? - splitConfig.visualDividerBounds.height() : - splitConfig.visualDividerBounds.width()); if (deviceProfile.isLandscape) { - primaryIconParams.gravity = TOP | START; - primaryIconView.setTranslationX( - primarySnapshotWidth - primaryIconView.getMeasuredWidth()); - primaryIconView.setTranslationY(0); - secondaryIconParams.gravity = TOP | START; - secondaryIconView.setTranslationX(primarySnapshotWidth + dividerBar); + // We calculate the "midpoint" of the thumbnail area, and place the icons there. + // This is the place where the thumbnail area splits by default, in a near-50/50 split. + // It is usually not exactly 50/50, due to insets/screen cutouts. + int fullscreenInsetThickness = deviceProfile.isSeascape() + ? deviceProfile.getInsets().right + : deviceProfile.getInsets().left; + int fullscreenMidpointFromBottom = ((deviceProfile.widthPx + - fullscreenInsetThickness) / 2); + float midpointFromBottomPct = (float) fullscreenMidpointFromBottom + / deviceProfile.widthPx; + float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx; + int spaceAboveSnapshots = 0; + int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots; + int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness + * midpointFromBottomPct); + int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); + + if (deviceProfile.isSeascape()) { + primaryIconParams.gravity = TOP | (isRtl ? END : START); + secondaryIconParams.gravity = TOP | (isRtl ? END : START); + if (splitConfig.initiatedFromSeascape) { + // if the split was initiated from seascape, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); + secondaryIconView.setTranslationX(bottomToMidpointOffset); + } else { + // if not, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset + - taskIconHeight); + secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); + } + } else { + primaryIconParams.gravity = TOP | (isRtl ? START : END); + secondaryIconParams.gravity = TOP | (isRtl ? START : END); + if (!splitConfig.initiatedFromSeascape) { + // if the split was initiated from landscape, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationX(-bottomToMidpointOffset); + secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); + } else { + // if not, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); + secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset + + taskIconHeight); + } + } } else { primaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - primaryIconView.setTranslationX(-(primaryIconView.getMeasuredWidth()) / 2f); - primaryIconView.setTranslationY(0); - + // shifts icon half a width left (height is used here since icons are square) + primaryIconView.setTranslationX(-(taskIconHeight / 2f)); secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - secondaryIconView.setTranslationX(secondaryIconView.getMeasuredWidth() / 2f); + secondaryIconView.setTranslationX(taskIconHeight / 2f); } + primaryIconView.setTranslationY(0); secondaryIconView.setTranslationY(0); + primaryIconView.setLayoutParams(primaryIconParams); secondaryIconView.setLayoutParams(secondaryIconParams); } diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java index de5f99cd18..74b6a5b28e 100644 --- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java +++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java @@ -20,11 +20,9 @@ import static android.view.Gravity.BOTTOM; import static android.view.Gravity.CENTER_VERTICAL; import static android.view.Gravity.END; import static android.view.Gravity.START; -import static android.view.Gravity.TOP; import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; import android.content.res.Resources; @@ -107,21 +105,25 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler { return new PointF(-margin, margin); } + + @Override - public Pair setDwbLayoutParamsAndGetTranslations(int taskViewWidth, + public Pair getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner) { + boolean isRtl = banner.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; float translationX = 0; float translationY = 0; FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); banner.setPivotX(0); banner.setPivotY(0); banner.setRotation(getDegreesRotated()); + translationX = taskViewWidth - banner.getHeight(); FrameLayout.LayoutParams snapshotParams = (FrameLayout.LayoutParams) thumbnailViews[0] .getLayoutParams(); - bannerParams.gravity = BOTTOM | END; - translationX = taskViewWidth - banner.getHeight(); + bannerParams.gravity = BOTTOM | (isRtl ? END : START); + if (splitBounds == null) { // Single, fullscreen case bannerParams.width = taskViewHeight - snapshotParams.topMargin; @@ -131,19 +133,22 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler { // Set correct width if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[1].getMeasuredHeight(); - } else { bannerParams.width = thumbnailViews[0].getMeasuredHeight(); + } else { + bannerParams.width = thumbnailViews[1].getMeasuredHeight(); } // Set translations if (desiredTaskId == splitBounds.rightBottomTaskId) { - translationY = -(taskViewHeight - snapshotParams.topMargin) - * (1f - splitBounds.leftTaskPercent) - + banner.getHeight(); + translationY = banner.getHeight(); } if (desiredTaskId == splitBounds.leftTopTaskId) { - translationY = banner.getHeight(); + float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically + ? (1f - splitBounds.topTaskPercent) + : (1f - splitBounds.leftTaskPercent); + translationY = banner.getHeight() + - ((taskViewHeight - snapshotParams.topMargin) + * bottomRightTaskPlusDividerPercent); } return new Pair<>(translationX, translationY); } @@ -158,33 +163,61 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler { // Add "right" option which is actually the top return Collections.singletonList(new SplitPositionOption( R.drawable.ic_split_right, R.string.split_screen_position_right, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); + STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); } @Override - public void setIconAndSnapshotParams(View mIconView, int taskIconMargin, int taskIconHeight, - FrameLayout.LayoutParams snapshotParams, boolean isRtl) { - FrameLayout.LayoutParams iconParams = - (FrameLayout.LayoutParams) mIconView.getLayoutParams(); + public void setTaskIconParams(FrameLayout.LayoutParams iconParams, + int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL; iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2; iconParams.rightMargin = 0; - iconParams.topMargin = snapshotParams.topMargin / 2; + iconParams.topMargin = thumbnailTopMargin / 2; } @Override public void setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - boolean isRtl, DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { + int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, + DeviceProfile deviceProfile, StagedSplitBounds splitConfig) { super.setSplitIconParams(primaryIconView, secondaryIconView, taskIconHeight, - primarySnapshotWidth, primarySnapshotHeight, isRtl, deviceProfile, splitConfig); + primarySnapshotWidth, primarySnapshotHeight, groupedTaskViewHeight, + groupedTaskViewWidth, isRtl, deviceProfile, splitConfig); FrameLayout.LayoutParams primaryIconParams = (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); FrameLayout.LayoutParams secondaryIconParams = (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams(); - primaryIconParams.gravity = (isRtl ? END : START) | TOP; - secondaryIconParams.gravity = (isRtl ? END : START) | TOP; + // We calculate the "midpoint" of the thumbnail area, and place the icons there. + // This is the place where the thumbnail area splits by default, in a near-50/50 split. + // It is usually not exactly 50/50, due to insets/screen cutouts. + int fullscreenInsetThickness = deviceProfile.getInsets().top; + int fullscreenMidpointFromBottom = ((deviceProfile.heightPx + - fullscreenInsetThickness) / 2); + float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx; + float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx; + int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx; + int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots; + int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct); + int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); + + primaryIconParams.gravity = BOTTOM | (isRtl ? END : START); + secondaryIconParams.gravity = BOTTOM | (isRtl ? END : START); + primaryIconView.setTranslationX(0); + secondaryIconView.setTranslationX(0); + if (splitConfig.initiatedFromSeascape) { + // if the split was initiated from seascape, + // the task on the right (secondary) is slightly larger + primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset); + secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset + + taskIconHeight); + } else { + // if not, + // the task on the left (primary) is slightly larger + primaryIconView.setTranslationY(-bottomToMidpointOffset); + secondaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight); + } + primaryIconView.setLayoutParams(primaryIconParams); secondaryIconView.setLayoutParams(secondaryIconParams); } diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java index 20d2ad36a5..17bbdf1769 100644 --- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java +++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java @@ -21,7 +21,9 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; +import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_LONGPRESS; import android.graphics.PointF; @@ -39,6 +41,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.Workspace; import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.TestProtocol; @@ -61,7 +64,7 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe private final Rect mTempRect = new Rect(); private final Launcher mLauncher; - private final Workspace mWorkspace; + private final Workspace mWorkspace; private final PointF mTouchDownPoint = new PointF(); private final float mTouchSlop; @@ -69,7 +72,7 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe private final GestureDetector mGestureDetector; - public WorkspaceTouchListener(Launcher launcher, Workspace workspace) { + public WorkspaceTouchListener(Launcher launcher, Workspace workspace) { mLauncher = launcher; mWorkspace = workspace; // Use twice the touch slop as we are looking for long press which is more @@ -118,6 +121,9 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe mLongPressState = STATE_COMPLETED; } + boolean isInAllAppsBottomSheet = mLauncher.isInState(ALL_APPS) + && mLauncher.getDeviceProfile().isTablet; + final boolean result; if (mLongPressState == STATE_COMPLETED) { // We have handled the touch, so workspace does not need to know anything anymore. @@ -133,8 +139,9 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe result = true; } else { - // We don't want to handle touch, let workspace handle it as usual. - result = false; + // We don't want to handle touch unless we're in AllApps bottom sheet, let workspace + // handle it as usual. + result = isInAllAppsBottomSheet; } if (action == ACTION_UP || action == ACTION_POINTER_UP) { @@ -150,6 +157,19 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe if (action == ACTION_UP || action == ACTION_CANCEL) { cancelLongPress(); } + if (action == ACTION_UP && isInAllAppsBottomSheet) { + mLauncher.getStateManager().goToState(NORMAL); + mLauncher.getStatsLogManager().logger() + .withSrcState(ALL_APPS.statsLogOrdinal) + .withDstState(NORMAL.statsLogOrdinal) + .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder() + .setWorkspace( + LauncherAtom.WorkspaceContainer.newBuilder() + .setPageIndex( + mLauncher.getWorkspace().getCurrentPage())) + .build()) + .log(LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE); + } return result; } diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java index c050c6ca87..7c73be51c6 100644 --- a/src/com/android/launcher3/util/DisplayController.java +++ b/src/com/android/launcher3/util/DisplayController.java @@ -15,42 +15,47 @@ */ package com.android.launcher3.util; +import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; +import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE; import static com.android.launcher3.Utilities.dpiFromPx; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import static com.android.launcher3.util.WindowManagerCompat.MIN_TABLET_WIDTH; - -import static java.util.Collections.emptyMap; +import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter; +import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ComponentCallbacks; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.Rect; import android.hardware.display.DisplayManager; -import android.hardware.display.DisplayManager.DisplayListener; import android.os.Build; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.view.Display; import androidx.annotation.AnyThread; import androidx.annotation.UiThread; -import androidx.annotation.WorkerThread; +import com.android.launcher3.ResourceUtils; import com.android.launcher3.Utilities; -import com.android.launcher3.uioverrides.ApiWrapper; +import com.android.launcher3.logging.StatsLogManager.LauncherEvent; +import com.android.launcher3.util.window.CachedDisplayInfo; +import com.android.launcher3.util.window.WindowManagerProxy; +import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Map; +import java.util.Collections; import java.util.Objects; import java.util.Set; @@ -58,7 +63,7 @@ import java.util.Set; * Utility class to cache properties of default display to avoid a system RPC on every call. */ @SuppressLint("NewApi") -public class DisplayController implements DisplayListener, ComponentCallbacks, SafeCloseable { +public class DisplayController implements ComponentCallbacks, SafeCloseable { private static final String TAG = "DisplayController"; @@ -67,22 +72,29 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S public static final int CHANGE_ACTIVE_SCREEN = 1 << 0; public static final int CHANGE_ROTATION = 1 << 1; - public static final int CHANGE_FRAME_DELAY = 1 << 2; - public static final int CHANGE_DENSITY = 1 << 3; - public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 4; + public static final int CHANGE_DENSITY = 1 << 2; + public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3; + public static final int CHANGE_NAVIGATION_MODE = 1 << 4; public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION - | CHANGE_FRAME_DELAY | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS; + | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE; + + private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; + private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode"; + private static final String TARGET_OVERLAY_PACKAGE = "android"; private final Context mContext; private final DisplayManager mDM; // Null for SDK < S private final Context mWindowContext; + // The callback in this listener updates DeviceProfile, which other listeners might depend on private DisplayInfoChangeListener mPriorityListener; private final ArrayList mListeners = new ArrayList<>(); + private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent); + private Info mInfo; private boolean mDestroyed = false; @@ -96,29 +108,23 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S mWindowContext.registerComponentCallbacks(this); } else { mWindowContext = null; - SimpleBroadcastReceiver configChangeReceiver = - new SimpleBroadcastReceiver(this::onConfigChanged); - mContext.registerReceiver(configChangeReceiver, - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); + mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED); } + + // Initialize navigation mode change listener + mContext.registerReceiver(mReceiver, + getPackageFilter(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED)); + + WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context); mInfo = new Info(getDisplayInfoContext(display), display, - getInternalDisplays(mDM), emptyMap()); - mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler()); + wmProxy, wmProxy.estimateInternalDisplayBounds(context)); } - private static ArrayMap getInternalDisplays( - DisplayManager displayManager) { - Display[] displays = displayManager.getDisplays(); - ArrayMap internalDisplays = new ArrayMap<>(); - for (Display display : displays) { - if (ApiWrapper.isInternalDisplay(display)) { - Point size = new Point(); - display.getRealSize(size); - internalDisplays.put(ApiWrapper.getUniqueId(display), - new PortraitSize(size.x, size.y)); - } - } - return internalDisplays; + /** + * Returns the current navigation mode + */ + public static NavigationMode getNavigationMode(Context context) { + return INSTANCE.get(context).getInfo().navigationMode; } @Override @@ -129,36 +135,6 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S } else { // TODO: unregister broadcast receiver } - mDM.unregisterDisplayListener(this); - } - - @Override - public final void onDisplayAdded(int displayId) { } - - @Override - public final void onDisplayRemoved(int displayId) { } - - @WorkerThread - @Override - public final void onDisplayChanged(int displayId) { - if (displayId != DEFAULT_DISPLAY) { - return; - } - Display display = mDM.getDisplay(DEFAULT_DISPLAY); - if (display == null) { - return; - } - if (Utilities.ATLEAST_S) { - // Only check for refresh rate. Everything else comes from component callbacks - if (getSingleFrameMs(display) == mInfo.singleFrameMs) { - return; - } - } - handleInfoChange(display); - } - - public static int getSingleFrameMs(Context context) { - return INSTANCE.get(context).getInfo().singleFrameMs; } /** @@ -175,15 +151,20 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S void onDisplayInfoChanged(Context context, Info info, int flags); } - /** - * Only used for pre-S - */ - private void onConfigChanged(Intent intent) { + private void onIntent(Intent intent) { if (mDestroyed) { return; } - Configuration config = mContext.getResources().getConfiguration(); - if (mInfo.fontScale != config.fontScale || mInfo.densityDpi != config.densityDpi) { + boolean reconfigure = false; + if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) { + reconfigure = true; + } else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { + Configuration config = mContext.getResources().getConfiguration(); + reconfigure = mInfo.fontScale != config.fontScale + || mInfo.densityDpi != config.densityDpi; + } + + if (reconfigure) { Log.d(TAG, "Configuration changed, notifying listeners"); Display display = mDM.getDisplay(DEFAULT_DISPLAY); if (display != null) { @@ -231,15 +212,17 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S @AnyThread private void handleInfoChange(Display display) { + WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext); Info oldInfo = mInfo; Context displayContext = getDisplayInfoContext(display); - Info newInfo = new Info(displayContext, display, - oldInfo.mInternalDisplays, oldInfo.mPerDisplayBounds); + Info newInfo = new Info(displayContext, display, wmProxy, oldInfo.mPerDisplayBounds); - if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) { + if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale + || newInfo.navigationMode != oldInfo.navigationMode) { // Cache may not be valid anymore, recreate without cache - newInfo = new Info(displayContext, display, getInternalDisplays(mDM), emptyMap()); + newInfo = new Info(displayContext, display, wmProxy, + wmProxy.estimateInternalDisplayBounds(displayContext)); } int change = 0; @@ -249,18 +232,19 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S if (newInfo.rotation != oldInfo.rotation) { change |= CHANGE_ROTATION; } - if (newInfo.singleFrameMs != oldInfo.singleFrameMs) { - change |= CHANGE_FRAME_DELAY; - } if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) { change |= CHANGE_DENSITY; } + if (newInfo.navigationMode != oldInfo.navigationMode) { + change |= CHANGE_NAVIGATION_MODE; + } if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)) { change |= CHANGE_SUPPORTED_BOUNDS; - PortraitSize realSize = new PortraitSize(newInfo.currentSize.x, newInfo.currentSize.y); - PortraitSize expectedSize = oldInfo.mInternalDisplays.get( - ApiWrapper.getUniqueId(display)); + Point currentS = newInfo.currentSize; + Pair cachedBounds = + oldInfo.mPerDisplayBounds.get(newInfo.displayId); + Point expectedS = cachedBounds == null ? null : cachedBounds.first.size; if (newInfo.supportedBounds.size() != oldInfo.supportedBounds.size()) { Log.e("b/198965093", "Inconsistent number of displays" @@ -268,8 +252,12 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S + "\noldInfo.supportedBounds: " + oldInfo.supportedBounds + "\nnewInfo.supportedBounds: " + newInfo.supportedBounds); } - if (!realSize.equals(expectedSize) && display.getState() == Display.STATE_OFF) { - Log.e("b/198965093", "Display size changed while display is off, ignoring change"); + if (expectedS != null + && (Math.min(currentS.x, currentS.y) != Math.min(expectedS.x, expectedS.y) + || Math.max(currentS.x, currentS.y) != Math.max(expectedS.x, expectedS.y)) + && display.getState() == Display.STATE_OFF) { + Log.e("b/198965093", + "Display size changed while display is off, ignoring change"); return; } } @@ -285,98 +273,109 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S if (mPriorityListener != null) { mPriorityListener.onDisplayInfoChanged(context, mInfo, flags); } - for (int i = mListeners.size() - 1; i >= 0; i--) { + + int count = mListeners.size(); + for (int i = 0; i < count; i++) { mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags); } } public static class Info { - public final int singleFrameMs; - - // Configuration properties + // Cached property public final int rotation; + public final String displayId; + public final Point currentSize; + public final Rect cutout; + + // Configuration property public final float fontScale; - public final int densityDpi; + private final int densityDpi; + public final NavigationMode navigationMode; private final PortraitSize mScreenSizeDp; - public final Point currentSize; - - public String displayId; public final Set supportedBounds = new ArraySet<>(); - private final Map> mPerDisplayBounds = new ArrayMap<>(); - private final ArrayMap mInternalDisplays; + + private final ArrayMap> mPerDisplayBounds = + new ArrayMap<>(); public Info(Context context, Display display) { - this(context, display, new ArrayMap<>(), emptyMap()); + /* don't need system overrides for external displays */ + this(context, display, new WindowManagerProxy(), new ArrayMap<>()); } - private Info(Context context, Display display, - ArrayMap internalDisplays, - Map> perDisplayBoundsCache) { - mInternalDisplays = internalDisplays; - rotation = display.getRotation(); + // Used for testing + public Info(Context context, Display display, + WindowManagerProxy wmProxy, + ArrayMap> perDisplayBoundsCache) { + CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(context, display); + rotation = displayInfo.rotation; + currentSize = displayInfo.size; + displayId = displayInfo.id; + cutout = displayInfo.cutout; Configuration config = context.getResources().getConfiguration(); fontScale = config.fontScale; densityDpi = config.densityDpi; mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp); + navigationMode = parseNavigationMode(context); - singleFrameMs = getSingleFrameMs(display); - currentSize = new Point(); - display.getRealSize(currentSize); + mPerDisplayBounds.putAll(perDisplayBoundsCache); + Pair cachedValue = mPerDisplayBounds.get(displayId); - displayId = ApiWrapper.getUniqueId(display); - Set currentSupportedBounds = - getSupportedBoundsForDisplay(display, currentSize); - mPerDisplayBounds.put(displayId, currentSupportedBounds); - supportedBounds.addAll(currentSupportedBounds); - - if (ApiWrapper.isInternalDisplay(display) && internalDisplays.size() > 1) { - int displayCount = internalDisplays.size(); - for (int i = 0; i < displayCount; i++) { - String displayKey = internalDisplays.keyAt(i); - if (TextUtils.equals(displayId, displayKey)) { - continue; - } - - Set displayBounds = perDisplayBoundsCache.get(displayKey); - if (displayBounds == null) { - // We assume densityDpi is the same across all internal displays - displayBounds = WindowManagerCompat.estimateDisplayProfiles( - context, internalDisplays.valueAt(i), densityDpi, - ApiWrapper.TASKBAR_DRAWN_IN_PROCESS); - } - - supportedBounds.addAll(displayBounds); - mPerDisplayBounds.put(displayKey, displayBounds); + WindowBounds realBounds = wmProxy.getRealBounds(context, display, displayInfo); + if (cachedValue == null) { + supportedBounds.add(realBounds); + } else { + // Verify that the real bounds are a match + WindowBounds expectedBounds = cachedValue.second[displayInfo.rotation]; + if (!realBounds.equals(expectedBounds)) { + WindowBounds[] clone = new WindowBounds[4]; + System.arraycopy(cachedValue.second, 0, clone, 0, 4); + clone[displayInfo.rotation] = realBounds; + cachedValue = Pair.create(displayInfo.normalize(), clone); + mPerDisplayBounds.put(displayId, cachedValue); } } - } - - private static Set getSupportedBoundsForDisplay(Display display, Point size) { - Point smallestSize = new Point(); - Point largestSize = new Point(); - display.getCurrentSizeRange(smallestSize, largestSize); - - int portraitWidth = Math.min(size.x, size.y); - int portraitHeight = Math.max(size.x, size.y); - Set result = new ArraySet<>(); - result.add(new WindowBounds(portraitWidth, portraitHeight, - smallestSize.x, largestSize.y)); - result.add(new WindowBounds(portraitHeight, portraitWidth, - largestSize.x, smallestSize.y)); - return result; + mPerDisplayBounds.values().forEach( + pair -> Collections.addAll(supportedBounds, pair.second)); + Log.d("b/211775278", "displayId: " + displayId + ", currentSize: " + currentSize); + Log.d("b/211775278", "perDisplayBounds: " + mPerDisplayBounds); } /** - * Returns true if the bounds represent a tablet + * Returns {@code true} if the bounds represent a tablet. */ public boolean isTablet(WindowBounds bounds) { - return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), - densityDpi) >= MIN_TABLET_WIDTH; + return smallestSizeDp(bounds) >= MIN_TABLET_WIDTH; } + + /** + * Returns smallest size in dp for given bounds. + */ + public float smallestSizeDp(WindowBounds bounds) { + return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), densityDpi); + } + + public int getDensityDpi() { + return densityDpi; + } + } + + /** + * Dumps the current state information + */ + public void dump(PrintWriter pw) { + Info info = mInfo; + pw.println("DisplayController.Info:"); + pw.println(" id=" + info.displayId); + pw.println(" rotation=" + info.rotation); + pw.println(" fontScale=" + info.fontScale); + pw.println(" densityDpi=" + info.densityDpi); + pw.println(" navigationMode=" + info.navigationMode.name()); + pw.println(" currentSize=" + info.currentSize); + pw.println(" supportedBounds=" + info.supportedBounds); } /** @@ -404,8 +403,35 @@ public class DisplayController implements DisplayListener, ComponentCallbacks, S } } - private static int getSingleFrameMs(Display display) { - float refreshRate = display.getRefreshRate(); - return refreshRate > 0 ? (int) (1000 / refreshRate) : 16; + public enum NavigationMode { + THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON), + TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON), + NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON); + + public final boolean hasGestures; + public final int resValue; + public final LauncherEvent launcherEvent; + + NavigationMode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) { + this.hasGestures = hasGestures; + this.resValue = resValue; + this.launcherEvent = launcherEvent; + } + } + + private static NavigationMode parseNavigationMode(Context context) { + int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME, + context.getResources(), INVALID_RESOURCE_HANDLE); + + if (modeInt == INVALID_RESOURCE_HANDLE) { + Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); + } else { + for (NavigationMode m : NavigationMode.values()) { + if (m.resValue == modeInt) { + return m; + } + } + } + return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : NavigationMode.THREE_BUTTONS; } } diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java index 6329540dc2..6978e0c2a4 100644 --- a/src/com/android/launcher3/util/Executors.java +++ b/src/com/android/launcher3/util/Executors.java @@ -19,6 +19,8 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Process; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; @@ -34,6 +36,9 @@ public class Executors { Math.max(Runtime.getRuntime().availableProcessors(), 2); private static final int KEEP_ALIVE = 1; + /** Dedicated executor instances for work depending on other packages. */ + private static final Map PACKAGE_EXECUTORS = new ConcurrentHashMap<>(); + /** * An {@link ThreadPoolExecutor} to be used with async task with no limit on the queue size. */ @@ -75,6 +80,17 @@ public class Executors { public static final LooperExecutor MODEL_EXECUTOR = new LooperExecutor(createAndStartNewLooper("launcher-loader")); + /** + * Returns and caches a single thread executor for a given package. + * + * @param packageName Package associated with the executor. + */ + public static LooperExecutor getPackageExecutor(String packageName) { + return PACKAGE_EXECUTORS.computeIfAbsent( + packageName, p -> new LooperExecutor( + createAndStartNewLooper(p, Process.THREAD_PRIORITY_DEFAULT))); + } + /** * A simple ThreadFactory to set the thread name and priority when used with executors. */ diff --git a/src/com/android/launcher3/util/FlagOp.java b/src/com/android/launcher3/util/FlagOp.java deleted file mode 100644 index bd40eb9fa8..0000000000 --- a/src/com/android/launcher3/util/FlagOp.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.launcher3.util; - -public interface FlagOp { - - FlagOp NO_OP = i -> i; - - int apply(int flags); - - static FlagOp addFlag(int flag) { - return i -> i | flag; - } - - static FlagOp removeFlag(int flag) { - return i -> i & ~flag; - } -} diff --git a/src/com/android/launcher3/util/GridOccupancy.java b/src/com/android/launcher3/util/GridOccupancy.java index 9c752a7043..13014608c4 100644 --- a/src/com/android/launcher3/util/GridOccupancy.java +++ b/src/com/android/launcher3/util/GridOccupancy.java @@ -7,7 +7,7 @@ import com.android.launcher3.model.data.ItemInfo; /** * Utility object to manage the occupancy in a grid. */ -public class GridOccupancy { +public class GridOccupancy extends AbsGridOccupancy { private final int mCountX; private final int mCountY; @@ -30,24 +30,7 @@ public class GridOccupancy { * @return true if a vacant cell was found */ public boolean findVacantCell(int[] vacantOut, int spanX, int spanY) { - for (int y = 0; (y + spanY) <= mCountY; y++) { - for (int x = 0; (x + spanX) <= mCountX; x++) { - boolean available = !cells[x][y]; - out: - for (int i = x; i < x + spanX; i++) { - for (int j = y; j < y + spanY; j++) { - available = available && !cells[i][j]; - if (!available) break out; - } - } - if (available) { - vacantOut[0] = x; - vacantOut[1] = y; - return true; - } - } - } - return false; + return super.findVacantCell(vacantOut, cells, mCountX, mCountY, spanX, spanY); } public void copyTo(GridOccupancy dest) { diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index 7917410b3b..b6af3140fa 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -19,6 +19,8 @@ package com.android.launcher3.util; import android.content.ComponentName; import android.os.UserHandle; +import androidx.annotation.NonNull; + import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -27,90 +29,64 @@ import com.android.launcher3.shortcuts.ShortcutKey; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.function.Predicate; /** * A utility class to check for {@link ItemInfo} */ -public interface ItemInfoMatcher { +public abstract class ItemInfoMatcher { /** * Empty component used for match testing */ - ComponentName EMPTY_COMPONENT = new ComponentName("", ""); + private static final ComponentName EMPTY_COMPONENT = new ComponentName("", ""); - boolean matches(ItemInfo info, ComponentName cn); - - /** - * Returns true if the itemInfo matches this check - */ - default boolean matchesInfo(ItemInfo info) { - if (info != null) { - ComponentName cn = info.getTargetComponent(); - return matches(info, cn != null ? cn : EMPTY_COMPONENT); - } else { - return false; - } + public static Predicate ofUser(UserHandle user) { + return info -> info != null && info.user.equals(user); } - /** - * Returns a new matcher with returns true if either this or {@param matcher} returns true. - */ - default ItemInfoMatcher or(ItemInfoMatcher matcher) { - return (info, cn) -> matches(info, cn) || matcher.matches(info, cn); + public static Predicate ofComponents( + HashSet components, UserHandle user) { + return info -> info != null && info.user.equals(user) + && components.contains(getNonNullComponent(info)); } - /** - * Returns a new matcher with returns true if both this and {@param matcher} returns true. - */ - default ItemInfoMatcher and(ItemInfoMatcher matcher) { - return (info, cn) -> matches(info, cn) && matcher.matches(info, cn); + public static Predicate ofPackages(Set packageNames, UserHandle user) { + return info -> info != null && info.user.equals(user) + && packageNames.contains(getNonNullComponent(info).getPackageName()); } - /** - * Returns a new matcher with returns the opposite value of this. - */ - default ItemInfoMatcher negate() { - return (info, cn) -> !matches(info, cn); - } - - static ItemInfoMatcher ofUser(UserHandle user) { - return (info, cn) -> info.user.equals(user); - } - - static ItemInfoMatcher ofComponents(HashSet components, UserHandle user) { - return (info, cn) -> components.contains(cn) && info.user.equals(user); - } - - static ItemInfoMatcher ofPackages(Set packageNames, UserHandle user) { - return (info, cn) -> packageNames.contains(cn.getPackageName()) && info.user.equals(user); - } - - static ItemInfoMatcher ofShortcutKeys(Set keys) { - return (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT + public static Predicate ofShortcutKeys(Set keys) { + return info -> info != null && info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT && keys.contains(ShortcutKey.fromItemInfo(info)); } /** * Returns a matcher for items within folders. */ - static ItemInfoMatcher forFolderMatch(ItemInfoMatcher childOperator) { - return (info, cn) -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream() - .anyMatch(childOperator::matchesInfo); + public static Predicate forFolderMatch(Predicate childOperator) { + return info -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream() + .anyMatch(childOperator); } /** * Returns a matcher for items with provided ids */ - static ItemInfoMatcher ofItemIds(IntSet ids) { - return (info, cn) -> ids.contains(info.id); + public static Predicate ofItemIds(IntSet ids) { + return info -> info != null && ids.contains(info.id); } /** * Returns a matcher for items with provided items */ - static ItemInfoMatcher ofItems(Collection items) { + public static Predicate ofItems(Collection items) { IntSet ids = new IntSet(); items.forEach(item -> ids.add(item.id)); return ofItemIds(ids); } + + private static ComponentName getNonNullComponent(@NonNull ItemInfo info) { + ComponentName cn = info.getTargetComponent(); + return cn != null ? cn : EMPTY_COMPONENT; + } } diff --git a/src/com/android/launcher3/util/MultiAdditivePropertyFactory.java b/src/com/android/launcher3/util/MultiAdditivePropertyFactory.java new file mode 100644 index 0000000000..50f7027407 --- /dev/null +++ b/src/com/android/launcher3/util/MultiAdditivePropertyFactory.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.util.ArrayMap; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; +import android.view.View; + +/** + * Allows to combine multiple values set by several sources. + * + * The various sources are meant to use [set], providing different `setterIndex` params. When it is + * not set, 0 is used. This is meant to cover the case multiple animations are going on at the same + * time. + * + * This class behaves similarly to [MultiValueAlpha], but is meant to be more abstract and reusable. + * It sets the addition of all values. + * + * @param Type where to apply the property. + */ +public class MultiAdditivePropertyFactory { + + private static final boolean DEBUG = false; + private static final String TAG = "MultiAdditivePropertyFactory"; + private final String mName; + private final ArrayMap mProperties = + new ArrayMap<>(); + + // This is an optimization for cases when set is called repeatedly with the same setterIndex. + private float mAggregationOfOthers = 0f; + private Integer mLastIndexSet = -1; + private final Property mProperty; + + public MultiAdditivePropertyFactory(String name, Property property) { + mName = name; + mProperty = property; + } + + /** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */ + public MultiAdditiveProperty get(Integer index) { + return mProperties.computeIfAbsent(index, + (k) -> new MultiAdditiveProperty(index, mName + "_" + index)); + } + + /** + * Each [setValue] will be aggregated with the other properties values created by the + * corresponding factory. + */ + class MultiAdditiveProperty extends FloatProperty { + private final int mInx; + private float mValue = 0f; + + MultiAdditiveProperty(int inx, String name) { + super(name); + mInx = inx; + } + + @Override + public void setValue(T obj, float newValue) { + if (mLastIndexSet != mInx) { + mAggregationOfOthers = 0f; + mProperties.forEach((key, property) -> { + if (key != mInx) { + mAggregationOfOthers += property.mValue; + } + }); + mLastIndexSet = mInx; + } + float lastAggregatedValue = mAggregationOfOthers + newValue; + mValue = newValue; + apply(obj, lastAggregatedValue); + + if (DEBUG) { + Log.d(TAG, "name=" + mName + + " newValue=" + newValue + " mInx=" + mInx + + " aggregated=" + lastAggregatedValue + " others= " + mProperties); + } + } + + @Override + public Float get(T view) { + // The scale of the view should match mLastAggregatedValue. Still, if it has been + // changed without using this property, it can differ. As this get method is usually + // used to set the starting point on an animation, this would result in some jumps + // when the view scale is different than the last aggregated value. To stay on the + // safe side, let's return the real view scale. + return mProperty.get(view); + } + + @Override + public String toString() { + return String.valueOf(mValue); + } + } + + protected void apply(View view, float value) { + mProperty.set(view, value); + } +} diff --git a/src/com/android/launcher3/util/MultiScalePropertyFactory.java b/src/com/android/launcher3/util/MultiScalePropertyFactory.java new file mode 100644 index 0000000000..a7e6cc8679 --- /dev/null +++ b/src/com/android/launcher3/util/MultiScalePropertyFactory.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.util.ArrayMap; +import android.util.FloatProperty; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.Utilities; + +/** + * Allows to combine multiple values set by several sources. + * + * The various sources are meant to use [set], providing different `setterIndex` params. When it is + * not set, 0 is used. This is meant to cover the case multiple animations are going on at the same + * time. + * + * This class behaves similarly to [MultiValueAlpha], but is meant to be more abstract and reusable. + * It sets the multiplication of all values, bounded to the max and the min values. + * + * @param Type where to apply the property. + */ +public class MultiScalePropertyFactory { + + private static final boolean DEBUG = false; + private static final String TAG = "MultiScaleProperty"; + private final String mName; + private final ArrayMap mProperties = + new ArrayMap(); + + // This is an optimization for cases when set is called repeatedly with the same setterIndex. + private float mMinOfOthers = 0; + private float mMaxOfOthers = 0; + private float mMultiplicationOfOthers = 0; + private Integer mLastIndexSet = -1; + private float mLastAggregatedValue = 1.0f; + + public MultiScalePropertyFactory(String name) { + mName = name; + } + + /** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */ + public MultiScaleProperty get(Integer index) { + return mProperties.computeIfAbsent(index, + (k) -> new MultiScaleProperty(index, mName + "_" + index)); + } + + /** + * Each [setValue] will be aggregated with the other properties values created by the + * corresponding factory. + */ + class MultiScaleProperty extends FloatProperty { + private final int mInx; + private float mValue = 1.0f; + + MultiScaleProperty(int inx, String name) { + super(name); + mInx = inx; + } + + @Override + public void setValue(T obj, float newValue) { + if (mLastIndexSet != mInx) { + mMinOfOthers = Float.MAX_VALUE; + mMaxOfOthers = Float.MIN_VALUE; + mMultiplicationOfOthers = 1.0f; + mProperties.forEach((key, property) -> { + if (key != mInx) { + mMinOfOthers = Math.min(mMinOfOthers, property.mValue); + mMaxOfOthers = Math.max(mMaxOfOthers, property.mValue); + mMultiplicationOfOthers *= property.mValue; + } + }); + mLastIndexSet = mInx; + } + float minValue = Math.min(mMinOfOthers, newValue); + float maxValue = Math.max(mMaxOfOthers, newValue); + float multValue = mMultiplicationOfOthers * newValue; + mLastAggregatedValue = Utilities.boundToRange(multValue, minValue, maxValue); + mValue = newValue; + apply(obj, mLastAggregatedValue); + + if (DEBUG) { + Log.d(TAG, "name=" + mName + + " newValue=" + newValue + " mInx=" + mInx + + " aggregated=" + mLastAggregatedValue + " others= " + mProperties); + } + } + + @Override + public Float get(T view) { + // The scale of the view should match mLastAggregatedValue. Still, if it has been + // changed without using this property, it can differ. As this get method is usually + // used to set the starting point on an animation, this would result in some jumps + // when the view scale is different than the last aggregated value. To stay on the + // safe side, let's return the real view scale. + return view.getScaleX(); + } + + @Override + public String toString() { + return String.valueOf(mValue); + } + } + + protected void apply(View view, float value) { + view.setScaleX(value); + view.setScaleY(value); + } +} diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java index 5ba0d30256..f4cf21efe5 100644 --- a/src/com/android/launcher3/util/OnboardingPrefs.java +++ b/src/com/android/launcher3/util/OnboardingPrefs.java @@ -20,7 +20,7 @@ import android.util.ArrayMap; import androidx.annotation.StringDef; -import com.android.launcher3.Launcher; +import com.android.launcher3.views.ActivityContext; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -29,23 +29,29 @@ import java.util.Map; /** * Stores and retrieves onboarding-related data via SharedPreferences. + * + * @param Context which owns these preferences. */ -public class OnboardingPrefs { +public class OnboardingPrefs { public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown"; public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count"; public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count"; public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen"; - public static final String SEARCH_EDU_SEEN = "launcher.search_edu_seen"; + public static final String SEARCH_KEYBOARD_EDU_SEEN = "launcher.search_edu_seen"; public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count"; + public static final String SEARCH_ONBOARDING_COUNT = "launcher.search_onboarding_count"; public static final String TASKBAR_EDU_SEEN = "launcher.taskbar_edu_seen"; + public static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count"; // When adding a new key, add it here as well, to be able to reset it from Developer Options. public static final Map ALL_PREF_KEYS = Map.of( "All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT }, "Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT, HOTSEAT_LONGPRESS_TIP_SEEN }, - "Search Education", new String[] { SEARCH_EDU_SEEN, SEARCH_SNACKBAR_COUNT }, - "Taskbar Education", new String[] { TASKBAR_EDU_SEEN } + "Search Education", new String[] { SEARCH_KEYBOARD_EDU_SEEN, SEARCH_SNACKBAR_COUNT, + SEARCH_ONBOARDING_COUNT}, + "Taskbar Education", new String[] { TASKBAR_EDU_SEEN }, + "All Apps Visited Count", new String[] {ALL_APPS_VISITED_COUNT} ); /** @@ -54,12 +60,11 @@ public class OnboardingPrefs { @StringDef(value = { HOME_BOUNCE_SEEN, HOTSEAT_LONGPRESS_TIP_SEEN, - SEARCH_EDU_SEEN, + SEARCH_KEYBOARD_EDU_SEEN, TASKBAR_EDU_SEEN }) @Retention(RetentionPolicy.SOURCE) - public @interface EventBoolKey { - } + public @interface EventBoolKey {} /** * Events that occur multiple times, which we count up to a max defined in {@link #MAX_COUNTS}. @@ -67,19 +72,23 @@ public class OnboardingPrefs { @StringDef(value = { HOME_BOUNCE_COUNT, HOTSEAT_DISCOVERY_TIP_COUNT, - SEARCH_SNACKBAR_COUNT + SEARCH_SNACKBAR_COUNT, + SEARCH_ONBOARDING_COUNT, + ALL_APPS_VISITED_COUNT }) @Retention(RetentionPolicy.SOURCE) - public @interface EventCountKey { - } + public @interface EventCountKey {} private static final Map MAX_COUNTS; static { - Map maxCounts = new ArrayMap<>(4); + Map maxCounts = new ArrayMap<>(5); maxCounts.put(HOME_BOUNCE_COUNT, 3); maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5); maxCounts.put(SEARCH_SNACKBAR_COUNT, 3); + // This is the sum of all onboarding cards. Currently there is only 1 card shown 3 times. + maxCounts.put(SEARCH_ONBOARDING_COUNT, 3); + maxCounts.put(ALL_APPS_VISITED_COUNT, 20); MAX_COUNTS = Collections.unmodifiableMap(maxCounts); } @@ -131,4 +140,14 @@ public class OnboardingPrefs { mSharedPrefs.edit().putInt(eventKey, count).apply(); return hasReachedMaxCount(count, eventKey); } + + /** + * Sets the event count to the given value. + * + * @return Whether we have now reached the max count. + */ + public boolean setEventCount(int count, @EventCountKey String eventKey) { + mSharedPrefs.edit().putInt(eventKey, count).apply(); + return hasReachedMaxCount(count, eventKey); + } } diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index 08ec5912a4..f42d30453b 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -202,7 +202,8 @@ public class PackageManagerHelper { public static Intent getStyleWallpapersIntent(Context context) { return new Intent(Intent.ACTION_SET_WALLPAPER).setComponent( new ComponentName(context.getString(R.string.wallpaper_picker_package), - "com.android.customization.picker.CustomizationPickerActivity")); + context.getString(R.string.custom_activity_picker) + )); } /** diff --git a/src/com/android/launcher3/util/RotationUtils.java b/src/com/android/launcher3/util/RotationUtils.java new file mode 100644 index 0000000000..3414a3de37 --- /dev/null +++ b/src/com/android/launcher3/util/RotationUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Utility methods based on {@code frameworks/base/core/java/android/util/RotationUtils.java} + */ +public class RotationUtils { + + /** + * Rotates an Rect according to the given rotation. + */ + public static void rotateRect(Rect rect, int rotation) { + switch (rotation) { + case ROTATION_0: + return; + case ROTATION_90: + rect.set(rect.top, rect.right, rect.bottom, rect.left); + return; + case ROTATION_180: + rect.set(rect.right, rect.bottom, rect.left, rect.top); + return; + case ROTATION_270: + rect.set(rect.bottom, rect.left, rect.top, rect.right); + return; + default: + throw new IllegalArgumentException("unknown rotation: " + rotation); + } + } + + /** + * Rotates an size according to the given rotation. + */ + public static void rotateSize(Point size, int rotation) { + switch (rotation) { + case ROTATION_0: + case ROTATION_180: + return; + case ROTATION_90: + case ROTATION_270: + size.set(size.y, size.x); + return; + default: + throw new IllegalArgumentException("unknown rotation: " + rotation); + } + } + + /** @return the rotation needed to rotate from oldRotation to newRotation. */ + public static int deltaRotation(int oldRotation, int newRotation) { + int delta = newRotation - oldRotation; + if (delta < 0) delta += 4; + return delta; + } +} diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java index 465a0e8b05..4dfa5ccdeb 100644 --- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java +++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java @@ -39,10 +39,17 @@ public class SimpleBroadcastReceiver extends BroadcastReceiver { * Helper method to register multiple actions */ public void register(Context context, String... actions) { + register(context, 0, actions); + } + + /** + * Helper method to register multiple actions with one or more {@code flags}. + */ + public void register(Context context, int flags, String... actions) { IntentFilter filter = new IntentFilter(); for (String action : actions) { filter.addAction(action); } - context.registerReceiver(this, filter); + context.registerReceiver(this, filter, flags); } } diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java index cb714b2a8f..6a336cce28 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -113,6 +113,14 @@ public final class SplitConfigurationOptions { * the bounds were originally in */ public final boolean appsStackedVertically; + /** + * If {@code true}, that means at the time of creation of this object, the phone was in + * seascape orientation. This is important on devices with insets, because they do not split + * evenly -- one of the insets must be slightly larger to account for the inset. + * From landscape, it is the leftTop task that expands slightly. + * From seascape, it is the rightBottom task that expands slightly. + */ + public final boolean initiatedFromSeascape; public final int leftTopTaskId; public final int rightBottomTaskId; @@ -128,17 +136,30 @@ public final class SplitConfigurationOptions { this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom, leftTopBounds.right, rightBottomBounds.top); appsStackedVertically = true; + initiatedFromSeascape = false; } else { // horizontal apps, vertical divider this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top, rightBottomBounds.left, leftTopBounds.bottom); appsStackedVertically = false; + // The following check is unreliable on devices without insets + // (initiatedFromSeascape will always be set to false.) This happens to be OK for + // all our current uses, but should be refactored. + // TODO: Create a more reliable check, or refactor how splitting works on devices + // with insets. + if (rightBottomBounds.width() > leftTopBounds.width()) { + initiatedFromSeascape = true; + } else { + initiatedFromSeascape = false; + } } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; - dividerWidthPercent = visualDividerBounds.width() / (float) rightBottomBounds.right; - dividerHeightPercent = visualDividerBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } } diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java index ac5368c8b4..7e6711f806 100644 --- a/src/com/android/launcher3/util/UiThreadHelper.java +++ b/src/com/android/launcher3/util/UiThreadHelper.java @@ -25,10 +25,13 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.util.Log; import android.view.View; +import android.view.WindowInsets; +import android.view.WindowInsetsController; import android.view.inputmethod.InputMethodManager; -import com.android.launcher3.BaseActivity; +import com.android.launcher3.Utilities; import com.android.launcher3.views.ActivityContext; /** @@ -49,6 +52,25 @@ public class UiThreadHelper { public static void hideKeyboardAsync(ActivityContext activityContext, IBinder token) { View root = activityContext.getDragLayer(); + if (Utilities.ATLEAST_R) { + Preconditions.assertUIThread(); + // Hide keyboard with WindowInsetsController if could. In case + // hideSoftInputFromWindow may get ignored by input connection being finished + // when the screen is off. + // + // In addition, inside IMF, the keyboards are closed asynchronously that launcher no + // longer need to post to the message queue. + final WindowInsetsController wic = root.getWindowInsetsController(); + WindowInsets insets = root.getRootWindowInsets(); + boolean isImeShown = insets != null && insets.isVisible(WindowInsets.Type.ime()); + if (wic != null && isImeShown) { + // this method cannot be called cross threads + wic.hide(WindowInsets.Type.ime()); + activityContext.getStatsLogManager().logger() + .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED); + return; + } + } // Since the launcher context cannot be accessed directly from callback, adding secondary // message to log keyboard close event asynchronously. Bundle mHideKeyboardLoggerMsg = new Bundle(); @@ -56,7 +78,7 @@ public class UiThreadHelper { STATS_LOGGER_KEY, Message.obtain( HANDLER.get(root.getContext()), - () -> BaseActivity.fromContext(root.getContext()) + () -> activityContext .getStatsLogManager() .logger() .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED) diff --git a/src/com/android/launcher3/util/ViewCache.java b/src/com/android/launcher3/util/ViewCache.java index 08b8744167..98e6822542 100644 --- a/src/com/android/launcher3/util/ViewCache.java +++ b/src/com/android/launcher3/util/ViewCache.java @@ -21,6 +21,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.R; + /** * Utility class to cache views at an activity level */ @@ -39,18 +41,26 @@ public class ViewCache { mCache.put(layoutId, entry); } + T result; if (entry.mCurrentSize > 0) { entry.mCurrentSize --; - T result = (T) entry.mViews[entry.mCurrentSize]; + result = (T) entry.mViews[entry.mCurrentSize]; entry.mViews[entry.mCurrentSize] = null; - return result; + } else { + result = (T) LayoutInflater.from(context).inflate(layoutId, parent, false); + result.setTag(R.id.cache_entry_tag_id, entry); } - - return (T) LayoutInflater.from(context).inflate(layoutId, parent, false); + return result; } public void recycleView(int layoutId, View view) { CacheEntry entry = mCache.get(layoutId); + if (entry != view.getTag(R.id.cache_entry_tag_id)) { + // Since this view was created, the cache has been reset. The view should not be + // recycled since this means the environment could also have changed, requiring new + // view setup. + return; + } if (entry != null && entry.mCurrentSize < entry.mMaxSize) { entry.mViews[entry.mCurrentSize] = view; entry.mCurrentSize++; diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java index 8a7cae90bc..43e98201f4 100644 --- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java +++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java @@ -14,6 +14,8 @@ import android.os.SystemClock; import android.util.Log; import android.view.animation.Interpolator; +import androidx.annotation.AnyThread; + import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.anim.Interpolators; @@ -30,7 +32,7 @@ public class WallpaperOffsetInterpolator extends BroadcastReceiver { // Don't use all the wallpaper for parallax until you have at least this many pages private static final int MIN_PARALLAX_PAGE_SPAN = 4; - private final Workspace mWorkspace; + private final Workspace mWorkspace; private final boolean mIsRtl; private final Handler mHandler; @@ -41,7 +43,7 @@ public class WallpaperOffsetInterpolator extends BroadcastReceiver { private boolean mLockedToDefaultPage; private int mNumScreens; - public WallpaperOffsetInterpolator(Workspace workspace) { + public WallpaperOffsetInterpolator(Workspace workspace) { mWorkspace = workspace; mIsRtl = Utilities.isRtl(workspace.getResources()); mHandler = new OffsetHandler(workspace.getContext()); @@ -182,6 +184,7 @@ public class WallpaperOffsetInterpolator extends BroadcastReceiver { } } + @AnyThread private void updateOffset() { Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0, mWindowToken).sendToTarget(); @@ -206,9 +209,12 @@ public class WallpaperOffsetInterpolator extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - mWallpaperIsLiveWallpaper = - WallpaperManager.getInstance(mWorkspace.getContext()).getWallpaperInfo() != null; - updateOffset(); + UI_HELPER_EXECUTOR.execute(() -> { + // Updating the boolean on a background thread is fine as the assignments are atomic + mWallpaperIsLiveWallpaper = + WallpaperManager.getInstance(context).getWallpaperInfo() != null; + updateOffset(); + }); } private static final int MSG_START_ANIMATION = 1; diff --git a/src/com/android/launcher3/util/WindowBounds.java b/src/com/android/launcher3/util/WindowBounds.java index c92770e8fd..a15679a822 100644 --- a/src/com/android/launcher3/util/WindowBounds.java +++ b/src/com/android/launcher3/util/WindowBounds.java @@ -33,19 +33,27 @@ public class WindowBounds { public final Rect bounds; public final Rect insets; public final Point availableSize; + public final int rotationHint; public WindowBounds(Rect bounds, Rect insets) { + this(bounds, insets, -1); + } + + public WindowBounds(Rect bounds, Rect insets, int rotationHint) { this.bounds = bounds; this.insets = insets; + this.rotationHint = rotationHint; availableSize = new Point(bounds.width() - insets.left - insets.right, bounds.height() - insets.top - insets.bottom); } - public WindowBounds(int width, int height, int availableWidth, int availableHeight) { + public WindowBounds(int width, int height, int availableWidth, int availableHeight, + int rotationHint) { this.bounds = new Rect(0, 0, width, height); this.availableSize = new Point(availableWidth, availableHeight); // We don't care about insets in this case this.insets = new Rect(0, 0, width - availableWidth, height - availableHeight); + this.rotationHint = rotationHint; } @Override diff --git a/src/com/android/launcher3/util/WindowManagerCompat.java b/src/com/android/launcher3/util/WindowManagerCompat.java deleted file mode 100644 index e1b9478832..0000000000 --- a/src/com/android/launcher3/util/WindowManagerCompat.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.util; - -import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE; -import static com.android.launcher3.Utilities.dpiFromPx; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Insets; -import android.graphics.Rect; -import android.os.Build; -import android.util.ArraySet; -import android.view.WindowInsets; -import android.view.WindowInsets.Type; -import android.view.WindowManager; -import android.view.WindowMetrics; - -import com.android.launcher3.R; -import com.android.launcher3.ResourceUtils; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.DisplayController.PortraitSize; - -import java.util.Collections; -import java.util.Set; - -/** - * Utility class to estimate window manager values - */ -@TargetApi(Build.VERSION_CODES.S) -public class WindowManagerCompat { - - public static final int MIN_TABLET_WIDTH = 600; - - /** - * Returns a set of supported render sizes for a internal display. - * This is a temporary workaround which assumes only nav-bar insets change across displays, and - * is only used until we eventually get the real values - * @param consumeTaskBar if true, it assumes that task bar is part of the app window - * and ignores any insets because of task bar. - */ - public static Set estimateDisplayProfiles( - Context windowContext, PortraitSize size, int densityDpi, boolean consumeTaskBar) { - if (!Utilities.ATLEAST_S) { - return Collections.emptySet(); - } - WindowInsets defaultInsets = windowContext.getSystemService(WindowManager.class) - .getMaximumWindowMetrics().getWindowInsets(); - boolean isGesturalMode = ResourceUtils.getIntegerByName( - "config_navBarInteractionMode", - windowContext.getResources(), - INVALID_RESOURCE_HANDLE) == 2; - - WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(defaultInsets); - Set result = new ArraySet<>(); - int swDP = (int) dpiFromPx(size.width, densityDpi); - boolean isTablet = swDP >= MIN_TABLET_WIDTH; - - final Insets portraitNav, landscapeNav; - if (isTablet && !consumeTaskBar) { - portraitNav = landscapeNav = Insets.of(0, 0, 0, windowContext.getResources() - .getDimensionPixelSize(R.dimen.taskbar_size)); - } else if (!isGesturalMode) { - portraitNav = Insets.of(0, 0, 0, - getSystemResource(windowContext, "navigation_bar_height", swDP)); - landscapeNav = isTablet - ? Insets.of(0, 0, 0, getSystemResource(windowContext, - "navigation_bar_height_landscape", swDP)) - : Insets.of(0, 0, getSystemResource(windowContext, - "navigation_bar_width", swDP), 0); - } else { - portraitNav = landscapeNav = Insets.of(0, 0, 0, 0); - } - - result.add(WindowBounds.fromWindowMetrics(new WindowMetrics( - new Rect(0, 0, size.width, size.height), - insetsBuilder.setInsets(Type.navigationBars(), portraitNav).build()))); - result.add(WindowBounds.fromWindowMetrics(new WindowMetrics( - new Rect(0, 0, size.height, size.width), - insetsBuilder.setInsets(Type.navigationBars(), landscapeNav).build()))); - return result; - } - - private static int getSystemResource(Context context, String key, int swDp) { - int resourceId = context.getResources().getIdentifier(key, "dimen", "android"); - if (resourceId > 0) { - Configuration conf = new Configuration(); - conf.smallestScreenWidthDp = swDp; - return context.createConfigurationContext(conf) - .getResources().getDimensionPixelSize(resourceId); - } - return 0; - } -} diff --git a/src/com/android/launcher3/util/window/CachedDisplayInfo.java b/src/com/android/launcher3/util/window/CachedDisplayInfo.java new file mode 100644 index 0000000000..06b9829270 --- /dev/null +++ b/src/com/android/launcher3/util/window/CachedDisplayInfo.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util.window; + +import static com.android.launcher3.util.RotationUtils.deltaRotation; +import static com.android.launcher3.util.RotationUtils.rotateRect; +import static com.android.launcher3.util.RotationUtils.rotateSize; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Surface; + +import java.util.Objects; + +/** + * Properties on a display + */ +public class CachedDisplayInfo { + + public final String id; + public final Point size; + public final int rotation; + public final Rect cutout; + + public CachedDisplayInfo() { + this(new Point(0, 0), 0); + } + + public CachedDisplayInfo(Point size, int rotation) { + this("", size, rotation, new Rect()); + } + + public CachedDisplayInfo(String id, Point size, int rotation, Rect cutout) { + this.id = id; + this.size = size; + this.rotation = rotation; + this.cutout = cutout; + } + + /** + * Returns a CachedDisplayInfo where the properties are normalized to {@link Surface#ROTATION_0} + */ + public CachedDisplayInfo normalize() { + if (rotation == Surface.ROTATION_0) { + return this; + } + Point newSize = new Point(size); + rotateSize(newSize, deltaRotation(rotation, Surface.ROTATION_0)); + + Rect newCutout = new Rect(cutout); + rotateRect(newCutout, deltaRotation(rotation, Surface.ROTATION_0)); + return new CachedDisplayInfo(id, newSize, Surface.ROTATION_0, newCutout); + } + + @Override + public String toString() { + return "CachedDisplayInfo{" + + "id='" + id + '\'' + + ", size=" + size + + ", rotation=" + rotation + + ", cutout=" + cutout + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CachedDisplayInfo)) return false; + CachedDisplayInfo that = (CachedDisplayInfo) o; + return rotation == that.rotation && Objects.equals(id, that.id) + && Objects.equals(size, that.size) && Objects.equals(cutout, + that.cutout); + } + + @Override + public int hashCode() { + return Objects.hash(id, size, rotation, cutout); + } +} diff --git a/src/com/android/launcher3/util/window/RefreshRateTracker.java b/src/com/android/launcher3/util/window/RefreshRateTracker.java new file mode 100644 index 0000000000..7814617b9e --- /dev/null +++ b/src/com/android/launcher3/util/window/RefreshRateTracker.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util.window; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.view.Display; + +import androidx.annotation.WorkerThread; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.SafeCloseable; + +/** + * Utility class to track refresh rate of the current device + */ +public class RefreshRateTracker implements DisplayListener, SafeCloseable { + + private static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(RefreshRateTracker::new); + + private int mSingleFrameMs = 1; + + private final DisplayManager mDM; + + private RefreshRateTracker(Context context) { + mDM = context.getSystemService(DisplayManager.class); + updateSingleFrameMs(); + mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler()); + } + + /** + * Returns the single frame time in ms + */ + public static int getSingleFrameMs(Context context) { + return INSTANCE.get(context).mSingleFrameMs; + } + + @Override + public final void onDisplayAdded(int displayId) { } + + @Override + public final void onDisplayRemoved(int displayId) { } + + @WorkerThread + @Override + public final void onDisplayChanged(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + updateSingleFrameMs(); + } + } + + private void updateSingleFrameMs() { + Display display = mDM.getDisplay(DEFAULT_DISPLAY); + if (display != null) { + float refreshRate = display.getRefreshRate(); + mSingleFrameMs = refreshRate > 0 ? (int) (1000 / refreshRate) : 16; + } + } + + @Override + public void close() { + mDM.unregisterDisplayListener(this); + } +} diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java new file mode 100644 index 0000000000..9665bf91c5 --- /dev/null +++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util.window; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; + +import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE; +import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT; +import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE; +import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE; +import static com.android.launcher3.ResourceUtils.STATUS_BAR_HEIGHT; +import static com.android.launcher3.ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE; +import static com.android.launcher3.ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT; +import static com.android.launcher3.Utilities.dpiFromPx; +import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; +import static com.android.launcher3.util.RotationUtils.deltaRotation; +import static com.android.launcher3.util.RotationUtils.rotateRect; +import static com.android.launcher3.util.RotationUtils.rotateSize; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.ArrayMap; +import android.util.Pair; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.Surface; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import com.android.launcher3.R; +import com.android.launcher3.ResourceUtils; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.WindowBounds; + +/** + * Utility class for mocking some window manager behaviours + */ +public class WindowManagerProxy implements ResourceBasedOverride { + + public static final int MIN_TABLET_WIDTH = 600; + + public static final MainThreadInitializedObject INSTANCE = + forOverride(WindowManagerProxy.class, R.string.window_manager_proxy_class); + + protected final boolean mTaskbarDrawnInProcess; + + /** + * Creates a new instance of proxy, applying any overrides + */ + public static WindowManagerProxy newInstance(Context context) { + return Overrides.getObject(WindowManagerProxy.class, context, + R.string.window_manager_proxy_class); + } + + public WindowManagerProxy() { + this(false); + } + + protected WindowManagerProxy(boolean taskbarDrawnInProcess) { + mTaskbarDrawnInProcess = taskbarDrawnInProcess; + } + + /** + * Returns a map of normalized info of internal displays to estimated window bounds + * for that display + */ + public ArrayMap> estimateInternalDisplayBounds( + Context context) { + Display[] displays = getDisplays(context); + ArrayMap> result = new ArrayMap<>(); + for (Display display : displays) { + if (isInternalDisplay(display)) { + Context displayContext = Utilities.ATLEAST_S + ? context.createWindowContext(display, TYPE_APPLICATION, null) + : context.createDisplayContext(display); + CachedDisplayInfo info = getDisplayInfo(displayContext, display).normalize(); + WindowBounds[] bounds = estimateWindowBounds(context, info); + result.put(info.id, Pair.create(info, bounds)); + } + } + return result; + } + + /** + * Returns the real bounds for the provided display after applying any insets normalization + */ + @TargetApi(Build.VERSION_CODES.R) + public WindowBounds getRealBounds(Context windowContext, + Display display, CachedDisplayInfo info) { + if (!Utilities.ATLEAST_R) { + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + + if (info.size.y > info.size.x) { + // Portrait + return new WindowBounds(info.size.x, info.size.y, smallestSize.x, largestSize.y, + info.rotation); + } else { + // Landscape + new WindowBounds(info.size.x, info.size.y, largestSize.x, smallestSize.y, + info.rotation); + } + } + + WindowMetrics wm = windowContext.getSystemService(WindowManager.class) + .getMaximumWindowMetrics(); + + Rect insets = new Rect(); + normalizeWindowInsets(windowContext, wm.getWindowInsets(), insets); + return new WindowBounds(wm.getBounds(), insets, info.rotation); + } + + /** + * Returns an updated insets, accounting for various Launcher UI specific overrides like taskbar + */ + @TargetApi(Build.VERSION_CODES.R) + public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets, + Rect outInsets) { + if (!Utilities.ATLEAST_R || !mTaskbarDrawnInProcess) { + outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(), + oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom()); + return oldInsets; + } + + WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets); + Insets navInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); + + Resources systemRes = context.getResources(); + Configuration config = systemRes.getConfiguration(); + + boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH; + boolean isGesture = isGestureNav(context); + boolean isPortrait = config.screenHeightDp > config.screenWidthDp; + + int bottomNav = isTablet + ? 0 + : (isPortrait + ? getDimenByName(systemRes, NAVBAR_HEIGHT) + : (isGesture + ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) + : 0)); + Insets newNavInsets = Insets.of(navInsets.left, navInsets.top, navInsets.right, bottomNav); + insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); + insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets); + + Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars()); + + + int statusBarHeight = getDimenByName(systemRes, + (isPortrait) ? STATUS_BAR_HEIGHT_PORTRAIT : STATUS_BAR_HEIGHT_LANDSCAPE, + STATUS_BAR_HEIGHT); + + Insets newStatusBarInsets = Insets.of( + statusBarInsets.left, + Math.max(statusBarInsets.top, statusBarHeight), + statusBarInsets.right, + statusBarInsets.bottom); + insetsBuilder.setInsets(WindowInsets.Type.statusBars(), newStatusBarInsets); + insetsBuilder.setInsetsIgnoringVisibility( + WindowInsets.Type.statusBars(), newStatusBarInsets); + + // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar + // would count towards it). This is used for the bottom protection in All Apps for example. + if (isGesture) { + Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); + Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, + oldTappableInsets.right, 0); + insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); + } + + WindowInsets result = insetsBuilder.build(); + Insets systemWindowInsets = result.getInsetsIgnoringVisibility( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + outInsets.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right, + systemWindowInsets.bottom); + return result; + } + + /** + * Returns true if the display is an internal displays + */ + protected boolean isInternalDisplay(Display display) { + return display.getDisplayId() == Display.DEFAULT_DISPLAY; + } + + /** + * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations + */ + public WindowBounds[] estimateWindowBounds(Context context, CachedDisplayInfo display) { + int densityDpi = context.getResources().getConfiguration().densityDpi; + int rotation = display.rotation; + Rect safeCutout = display.cutout; + + int minSize = Math.min(display.size.x, display.size.y); + int swDp = (int) dpiFromPx(minSize, densityDpi); + + Resources systemRes; + { + Configuration conf = new Configuration(); + conf.smallestScreenWidthDp = swDp; + systemRes = context.createConfigurationContext(conf).getResources(); + } + + boolean isTablet = swDp >= MIN_TABLET_WIDTH; + boolean isTabletOrGesture = isTablet + || (Utilities.ATLEAST_R && isGestureNav(context)); + + int statusBarHeightPortrait = getDimenByName(systemRes, + STATUS_BAR_HEIGHT_PORTRAIT, STATUS_BAR_HEIGHT); + int statusBarHeightLandscape = getDimenByName(systemRes, + STATUS_BAR_HEIGHT_LANDSCAPE, STATUS_BAR_HEIGHT); + + int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape; + + navBarHeightPortrait = isTablet + ? (mTaskbarDrawnInProcess + ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size)) + : getDimenByName(systemRes, NAVBAR_HEIGHT); + + navBarHeightLandscape = isTablet + ? (mTaskbarDrawnInProcess + ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size)) + : (isTabletOrGesture + ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0); + navbarWidthLandscape = isTabletOrGesture + ? 0 + : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); + + WindowBounds[] result = new WindowBounds[4]; + Point tempSize = new Point(); + for (int i = 0; i < 4; i++) { + int rotationChange = deltaRotation(rotation, i); + tempSize.set(display.size.x, display.size.y); + rotateSize(tempSize, rotationChange); + Rect bounds = new Rect(0, 0, tempSize.x, tempSize.y); + + int navBarHeight, navbarWidth, statusBarHeight; + if (tempSize.y > tempSize.x) { + navBarHeight = navBarHeightPortrait; + navbarWidth = 0; + statusBarHeight = statusBarHeightPortrait; + } else { + navBarHeight = navBarHeightLandscape; + navbarWidth = navbarWidthLandscape; + statusBarHeight = statusBarHeightLandscape; + } + + Rect insets = new Rect(safeCutout); + rotateRect(insets, rotationChange); + insets.top = Math.max(insets.top, statusBarHeight); + insets.bottom = Math.max(insets.bottom, navBarHeight); + + if (i == Surface.ROTATION_270 || i == Surface.ROTATION_180) { + // On reverse landscape (and in rare-case when the natural orientation of the + // device is landscape), navigation bar is on the right. + insets.left = Math.max(insets.left, navbarWidth); + } else { + insets.right = Math.max(insets.right, navbarWidth); + } + result[i] = new WindowBounds(bounds, insets, i); + } + return result; + } + + /** + * Wrapper around the utility method for easier emulation + */ + protected int getDimenByName(Resources res, String resName) { + return ResourceUtils.getDimenByName(resName, res, 0); + } + + /** + * Wrapper around the utility method for easier emulation + */ + protected int getDimenByName(Resources res, String resName, String fallback) { + int dimen = ResourceUtils.getDimenByName(resName, res, -1); + return dimen > -1 ? dimen : getDimenByName(res, fallback); + } + + protected boolean isGestureNav(Context context) { + return ResourceUtils.getIntegerByName("config_navBarInteractionMode", + context.getResources(), INVALID_RESOURCE_HANDLE) == 2; + } + + /** + * Returns a CachedDisplayInfo initialized for the current display + */ + @TargetApi(Build.VERSION_CODES.S) + public CachedDisplayInfo getDisplayInfo(Context displayContext, Display display) { + int rotation = getRotation(displayContext); + Rect cutoutRect = new Rect(); + Point size = new Point(); + if (Utilities.ATLEAST_S) { + WindowMetrics wm = displayContext.getSystemService(WindowManager.class) + .getMaximumWindowMetrics(); + DisplayCutout cutout = wm.getWindowInsets().getDisplayCutout(); + if (cutout != null) { + cutoutRect.set(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } + + size.set(wm.getBounds().right, wm.getBounds().bottom); + } else { + display.getRealSize(size); + } + return new CachedDisplayInfo(getDisplayId(display), size, rotation, cutoutRect); + } + + /** + * Returns a unique ID representing the display + */ + protected String getDisplayId(Display display) { + return Integer.toString(display.getDisplayId()); + } + + /** + * Returns rotation of the display associated with the context. + */ + public int getRotation(Context context) { + Display d = null; + if (Utilities.ATLEAST_R) { + try { + d = context.getDisplay(); + } catch (UnsupportedOperationException e) { + // Ignore + } + } + if (d == null) { + d = context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY); + } + return d.getRotation(); + } + + /** + * Returns all currently valid logical displays. + */ + protected Display[] getDisplays(Context context) { + return context.getSystemService(DisplayManager.class).getDisplays(); + } +} diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java index 8ac40b85b9..47503b118e 100644 --- a/src/com/android/launcher3/views/AbstractSlideInView.java +++ b/src/com/android/launcher3/views/AbstractSlideInView.java @@ -17,6 +17,8 @@ package com.android.launcher3.views; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import android.animation.Animator; @@ -113,9 +115,16 @@ public abstract class AbstractSlideInView return -1; } + /** + * Returns the range in height that the slide in view can be dragged. + */ + protected float getShiftRange() { + return mContent.getHeight(); + } + protected void setTranslationShift(float translationShift) { mTranslationShift = translationShift; - mContent.setTranslationY(mTranslationShift * mContent.getHeight()); + mContent.setTranslationY(mTranslationShift * getShiftRange()); if (mColorScrim != null) { mColorScrim.setAlpha(1 - mTranslationShift); } @@ -132,8 +141,7 @@ public abstract class AbstractSlideInView mSwipeDetector.setDetectableScrollConditions( directionsToDetectScroll, false); mSwipeDetector.onTouchEvent(ev); - return mSwipeDetector.isDraggingOrSettling() - || !getPopupContainer().isEventOverView(mContent, ev); + return mSwipeDetector.isDraggingOrSettling() || !isEventOverContent(ev); } @Override @@ -142,13 +150,23 @@ public abstract class AbstractSlideInView if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState() && !isOpeningAnimationRunning()) { // If we got ACTION_UP without ever starting swipe, close the panel. - if (!getPopupContainer().isEventOverView(mContent, ev)) { + if (!isEventOverContent(ev)) { close(true); } } return true; } + /** + * Returns {@code true} if the touch event is over the visible area of the bottom sheet. + * + * By default will check if the touch event is over {@code mContent}, subclasses should override + * this method if the visible area of the bottom sheet is different from {@code mContent}. + */ + protected boolean isEventOverContent(MotionEvent ev) { + return getPopupContainer().isEventOverView(mContent, ev); + } + private boolean isOpeningAnimationRunning() { return mIsOpen && mOpenCloseAnimator.isRunning(); } @@ -160,7 +178,7 @@ public abstract class AbstractSlideInView @Override public boolean onDrag(float displacement) { - float range = mContent.getHeight(); + float range = getShiftRange(); displacement = Utilities.boundToRange(displacement, 0, range); setTranslationShift(displacement / range); return true; @@ -168,7 +186,10 @@ public abstract class AbstractSlideInView @Override public void onDragEnd(float velocity) { - if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) { + float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet + ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS; + if ((mSwipeDetector.isFling(velocity) && velocity > 0) + || mTranslationShift > successfulShiftThreshold) { mScrollInterpolator = scrollInterpolatorForVelocity(velocity); mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration( velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift)); @@ -203,19 +224,24 @@ public abstract class AbstractSlideInView mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { + mOpenCloseAnimator.removeListener(this); onCloseComplete(); } }); if (mSwipeDetector.isIdleState()) { mOpenCloseAnimator .setDuration(defaultDuration) - .setInterpolator(Interpolators.ACCEL); + .setInterpolator(getIdleInterpolator()); } else { mOpenCloseAnimator.setInterpolator(mScrollInterpolator); } mOpenCloseAnimator.start(); } + protected Interpolator getIdleInterpolator() { + return Interpolators.ACCEL; + } + protected void onCloseComplete() { mIsOpen = false; getPopupContainer().removeView(this); diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index a2e4ad6809..93078e4cf6 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -25,12 +25,17 @@ import android.view.View.AccessibilityDelegate; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.allapps.search.SearchAdapterProvider; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.ViewCache; /** @@ -92,6 +97,13 @@ public interface ActivityContext { */ BaseDragLayer getDragLayer(); + /** + * The all apps container, if it exists in this context. + */ + default ActivityAllAppsContainerView getAppsView() { + return null; + } + DeviceProfile getDeviceProfile(); default ViewCache getViewCache() { @@ -123,18 +135,21 @@ public interface ActivityContext { return true; } - /** - * Returns whether we can show the IME for elements hosted by this ActivityContext. - */ - default boolean supportsIme() { - return true; - } - /** * Called just before logging the given item. */ default void applyOverwritesToLogItem(LauncherAtom.ItemInfo.Builder itemInfoBuilder) { } + /** Onboarding preferences for any onboarding data within this context. */ + default OnboardingPrefs getOnboardingPrefs() { + return null; + } + + /** Returns {@code true} if items are currently being bound within this context. */ + default boolean isBindingItems() { + return false; + } + /** * Returns the ActivityContext associated with the given Context, or throws an exception if * the Context is not associated with any ActivityContext. @@ -166,4 +181,24 @@ public interface ActivityContext { // No op. }; } + + @Nullable + default PopupDataProvider getPopupDataProvider() { + return null; + } + + @Nullable + default StringCache getStringCache() { + return null; + } + + /** + * Creates and returns {@link SearchAdapterProvider} for build variant specific search result + * views. + */ + @Nullable + default SearchAdapterProvider createSearchAdapterProvider( + ActivityAllAppsContainerView appsView) { + return null; + } } diff --git a/src/com/android/launcher3/views/AllAppsButton.java b/src/com/android/launcher3/views/AllAppsButton.java new file mode 100644 index 0000000000..b1e69c7de4 --- /dev/null +++ b/src/com/android/launcher3/views/AllAppsButton.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.views; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.icons.FastBitmapDrawable; + +/** + * Button in Taskbar that opens All Apps. + */ +public class AllAppsButton extends BubbleTextView { + + public AllAppsButton(Context context) { + this(context, null); + } + + public AllAppsButton(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + Context theme = new ContextThemeWrapper(context, R.style.AllAppsButtonTheme); + Bitmap bitmap = LauncherAppState.getInstance(context).getIconCache().getIconFactory() + .createScaledBitmapWithShadow(theme.getDrawable(R.drawable.ic_all_apps_button)); + setIcon(new FastBitmapDrawable(bitmap)); + } +} diff --git a/src/com/android/launcher3/views/AppLauncher.java b/src/com/android/launcher3/views/AppLauncher.java new file mode 100644 index 0000000000..19e66abd78 --- /dev/null +++ b/src/com/android/launcher3/views/AppLauncher.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.views; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; +import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS; + +import android.app.ActivityOptions; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Process; +import android.os.StrictMode; +import android.os.UserHandle; +import android.util.Log; +import android.view.Display; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.logging.InstanceIdSequence; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.util.ActivityOptionsWrapper; +import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.util.RunnableList; + +/** An {@link ActivityContext} that can also launch app activities and shortcuts safely. */ +public interface AppLauncher extends ActivityContext { + + String TAG = "AppLauncher"; + + /** + * Safely starts an activity. + * + * @param v View starting the activity. + * @param intent Base intent being launched. + * @param item Item associated with the view. + * @return {@code true} if the activity starts successfully. + */ + default boolean startActivitySafely( + View v, Intent intent, @Nullable ItemInfo item) { + + Context context = (Context) this; + if (isAppBlockedForSafeMode() && !PackageManagerHelper.isSystemApp(context, intent)) { + Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); + return false; + } + + Bundle optsBundle = (v != null) ? getActivityLaunchOptions(v, item).toBundle() : null; + UserHandle user = item == null ? null : item.user; + + // Prepare intent + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (v != null) { + intent.setSourceBounds(Utilities.getViewBounds(v)); + } + try { + boolean isShortcut = (item instanceof WorkspaceItemInfo) + && (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT + || item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) + && !((WorkspaceItemInfo) item).isPromise(); + if (isShortcut) { + // Shortcuts need some special checks due to legacy reasons. + startShortcutIntentSafely(intent, optsBundle, item); + } else if (user == null || user.equals(Process.myUserHandle())) { + // Could be launching some bookkeeping activity + context.startActivity(intent, optsBundle); + } else { + context.getSystemService(LauncherApps.class).startMainActivity( + intent.getComponent(), user, intent.getSourceBounds(), optsBundle); + } + if (item != null) { + InstanceId instanceId = new InstanceIdSequence().newInstanceId(); + logAppLaunch(getStatsLogManager(), item, instanceId); + } + return true; + } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { + Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e); + } + return false; + } + + /** Returns {@code true} if an app launch is blocked due to safe mode. */ + default boolean isAppBlockedForSafeMode() { + return false; + } + + /** + * Creates and logs a new app launch event. + */ + default void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info, + InstanceId instanceId) { + statsLogManager.logger().withItemInfo(info).withInstanceId(instanceId) + .log(LAUNCHER_APP_LAUNCH_TAP); + } + + /** + * Returns launch options for an Activity. + * + * @param v View initiating a launch. + * @param item Item associated with the view. + */ + default ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { + int left = 0, top = 0; + int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); + if (v instanceof BubbleTextView) { + // Launch from center of icon, not entire view + Drawable icon = ((BubbleTextView) v).getIcon(); + if (icon != null) { + Rect bounds = icon.getBounds(); + left = (width - bounds.width()) / 2; + top = v.getPaddingTop(); + width = bounds.width(); + height = bounds.height(); + } + } + ActivityOptions options = + ActivityOptions.makeClipRevealAnimation(v, left, top, width, height); + + options.setLaunchDisplayId( + (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId() + : Display.DEFAULT_DISPLAY); + RunnableList callback = new RunnableList(); + return new ActivityOptionsWrapper(options, callback); + } + + /** + * Safely launches an intent for a shortcut. + * + * @param intent Intent to start. + * @param optsBundle Optional launch arguments. + * @param info Shortcut information. + */ + default void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info) { + try { + StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy(); + try { + // Temporarily disable deathPenalty on all default checks. For eg, shortcuts + // containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure + // is enabled by default on NYC. + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll() + .penaltyLog().build()); + + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + String id = ((WorkspaceItemInfo) info).getDeepShortcutId(); + String packageName = intent.getPackage(); + startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user); + } else { + // Could be launching some bookkeeping activity + ((Context) this).startActivity(intent, optsBundle); + } + } finally { + StrictMode.setVmPolicy(oldPolicy); + } + } catch (SecurityException e) { + if (!onErrorStartingShortcut(intent, info)) { + throw e; + } + } + } + + /** + * A wrapper around the platform method with Launcher specific checks. + */ + default void startShortcut(String packageName, String id, Rect sourceBounds, + Bundle startActivityOptions, UserHandle user) { + if (GO_DISABLE_WIDGETS) { + return; + } + try { + ((Context) this).getSystemService(LauncherApps.class).startShortcut(packageName, id, + sourceBounds, startActivityOptions, user); + } catch (SecurityException | IllegalStateException e) { + Log.e(TAG, "Failed to start shortcut", e); + } + } + + /** + * Invoked when a shortcut fails to launch. + * @param intent Shortcut intent that failed to start. + * @param info Shortcut information. + * @return {@code true} if the error is handled by this callback. + */ + default boolean onErrorStartingShortcut(Intent intent, ItemInfo info) { + return false; + } +} diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java index ce26a66da5..8d16a8d982 100644 --- a/src/com/android/launcher3/views/ArrowTipView.java +++ b/src/com/android/launcher3/views/ArrowTipView.java @@ -137,6 +137,21 @@ public class ArrowTipView extends AbstractFloatingView { * @return The tooltip. */ public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) { + return show(text, gravity, arrowMarginStart, top, true); + } + + /** + * Show the ArrowTipView (tooltip) center, start, or end aligned. + * + * @param text The text to be shown in the tooltip. + * @param gravity The gravity aligns the tooltip center, start, or end. + * @param arrowMarginStart The margin from start to place arrow (ignored if center) + * @param top The Y coordinate of the bottom of tooltip. + * @param shouldAutoClose If Tooltip should be auto close. + * @return The tooltip. + */ + public ArrowTipView show( + String text, int gravity, int arrowMarginStart, int top, boolean shouldAutoClose) { ((TextView) findViewById(R.id.text)).setText(text); ViewGroup parent = mActivity.getDragLayer(); parent.addView(this); @@ -166,7 +181,9 @@ public class ArrowTipView extends AbstractFloatingView { post(() -> setY(top - (mIsPointingUp ? 0 : getHeight()))); mIsOpen = true; - mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); + if (shouldAutoClose) { + mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); + } setAlpha(0); animate() .alpha(1f) @@ -189,11 +206,33 @@ public class ArrowTipView extends AbstractFloatingView { * @return The tool tip view. {@code null} if the tip can not be shown. */ @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) { + return showAtLocation( + text, + arrowXCoord, + /* yCoordDownPointingTip= */ yCoord, + /* yCoordUpPointingTip= */ yCoord, + /* shouldAutoClose= */ true); + } + + /** + * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it + * cannot fit on screen in the requested orientation. + * + * @param text The text to be shown in the tooltip. + * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the + * center of tooltip unless the tooltip goes beyond screen margin. + * @param yCoord The Y coordinate of the pointed tip end of the tooltip. + * @param shouldAutoClose If Tooltip should be auto close. + * @return The tool tip view. {@code null} if the tip can not be shown. + */ + @Nullable public ArrowTipView showAtLocation( + String text, @Px int arrowXCoord, @Px int yCoord, boolean shouldAutoClose) { return showAtLocation( text, arrowXCoord, /* yCoordDownPointingTip= */ yCoord, - /* yCoordUpPointingTip= */ yCoord); + /* yCoordUpPointingTip= */ yCoord, + /* shouldAutoClose= */ shouldAutoClose); } /** @@ -213,7 +252,8 @@ public class ArrowTipView extends AbstractFloatingView { text, arrowXCoord, /* yCoordDownPointingTip= */ rect.top - margin, - /* yCoordUpPointingTip= */ rect.bottom + margin); + /* yCoordUpPointingTip= */ rect.bottom + margin, + /* shouldAutoClose= */ true); } /** @@ -227,10 +267,11 @@ public class ArrowTipView extends AbstractFloatingView { * tooltip is placed pointing downwards. * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the * tooltip is placed pointing upwards. + * @param shouldAutoClose If Tooltip should be auto close. * @return The tool tip view. {@code null} if the tip can not be shown. */ @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord, - @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip) { + @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip, boolean shouldAutoClose) { ViewGroup parent = mActivity.getDragLayer(); @Px int parentViewWidth = parent.getWidth(); @Px int parentViewHeight = parent.getHeight(); @@ -288,7 +329,9 @@ public class ArrowTipView extends AbstractFloatingView { }); mIsOpen = true; - mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); + if (shouldAutoClose) { + mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); + } setAlpha(0); animate() .alpha(1f) diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java index 6e44a43d61..f553fb4e8e 100644 --- a/src/com/android/launcher3/views/BaseDragLayer.java +++ b/src/com/android/launcher3/views/BaseDragLayer.java @@ -20,7 +20,7 @@ import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; -import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import android.annotation.TargetApi; import android.app.WallpaperManager; @@ -413,6 +413,14 @@ public abstract class BaseDragLayer coord, includeRootScroll); } + /** + * Similar to {@link #mapCoordInSelfToDescendant(View descendant, float[] coord)} + * but accepts a Rect instead of float[]. + */ + public void mapRectInSelfToDescendant(View descendant, Rect rect) { + Utilities.mapRectInSelfToDescendant(descendant, this, rect); + } + /** * Inverse of {@link #getDescendantCoordRelativeToSelf(View, float[])}. */ diff --git a/src/com/android/launcher3/views/BubbleTextHolder.java b/src/com/android/launcher3/views/BubbleTextHolder.java index 1cb27e1649..84f8049ded 100644 --- a/src/com/android/launcher3/views/BubbleTextHolder.java +++ b/src/com/android/launcher3/views/BubbleTextHolder.java @@ -16,20 +16,20 @@ package com.android.launcher3.views; import com.android.launcher3.BubbleTextView; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.ItemInfoWithIcon; /** * Views that contain {@link BubbleTextView} should implement this interface. */ -public interface BubbleTextHolder { +public interface BubbleTextHolder extends IconLabelDotView { BubbleTextView getBubbleText(); - /** - * Called when new {@link ItemInfo} is set to {@link BubbleTextView} - * - * @param itemInfo the new itemInfo - */ - default void onItemInfoUpdated(ItemInfoWithIcon itemInfo) { + @Override + default void setIconVisible(boolean visible) { + getBubbleText().setIconVisible(visible); + } + + @Override + default void setForceHideDot(boolean hide) { + getBubbleText().setForceHideDot(hide); } } diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java index a66b3f942e..d1f90e987f 100644 --- a/src/com/android/launcher3/views/ClipIconView.java +++ b/src/com/android/launcher3/views/ClipIconView.java @@ -147,8 +147,7 @@ public class ClipIconView extends View implements ClipPathView { * Update the icon UI to match the provided parameters during an animation frame */ public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, - int fgIconAlpha, boolean isOpening, View container, DeviceProfile dp, - boolean isVerticalBarLayout) { + int fgIconAlpha, boolean isOpening, View container, DeviceProfile dp) { MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams(); float dX = mIsRtl @@ -169,7 +168,7 @@ public class ClipIconView extends View implements ClipPathView { } update(rect, progress, shapeProgressStart, cornerRadius, fgIconAlpha, isOpening, scale, - minSize, lp, isVerticalBarLayout, dp); + minSize, lp, dp); container.setPivotX(0); container.setPivotY(0); @@ -181,7 +180,7 @@ public class ClipIconView extends View implements ClipPathView { private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, int fgIconAlpha, boolean isOpening, float scale, float minSize, - MarginLayoutParams parentLp, boolean isVerticalBarLayout, DeviceProfile dp) { + MarginLayoutParams parentLp, DeviceProfile dp) { float dX = mIsRtl ? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width) : rect.left - parentLp.getMarginStart(); @@ -193,7 +192,7 @@ public class ClipIconView extends View implements ClipPathView { float shapeRevealProgress = boundToRange(mapToRange(max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax, LINEAR), 0, 1); - if (isVerticalBarLayout) { + if (dp.isLandscape) { mOutline.right = (int) (rect.width() / scale); } else { mOutline.bottom = (int) (rect.height() / scale); @@ -218,16 +217,16 @@ public class ClipIconView extends View implements ClipPathView { mRevealAnimator.setCurrentFraction(shapeRevealProgress); } - float drawableScale = (isVerticalBarLayout ? mOutline.width() : mOutline.height()) + float drawableScale = (dp.isLandscape ? mOutline.width() : mOutline.height()) / minSize; - setBackgroundDrawableBounds(drawableScale, isVerticalBarLayout); + setBackgroundDrawableBounds(drawableScale, dp.isLandscape); if (isOpening) { // Center align foreground int height = mFinalDrawableBounds.height(); int width = mFinalDrawableBounds.width(); - int diffY = isVerticalBarLayout ? 0 + int diffY = dp.isLandscape ? 0 : (int) (((height * drawableScale) - height) / 2); - int diffX = isVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2) + int diffX = dp.isLandscape ? (int) (((width * drawableScale) - width) / 2) : 0; sTmpRect.set(mFinalDrawableBounds); sTmpRect.offset(diffX, diffY); @@ -247,11 +246,11 @@ public class ClipIconView extends View implements ClipPathView { invalidateOutline(); } - private void setBackgroundDrawableBounds(float scale, boolean isVerticalBarLayout) { + private void setBackgroundDrawableBounds(float scale, boolean isLandscape) { sTmpRect.set(mFinalDrawableBounds); Utilities.scaleRectAboutCenter(sTmpRect, scale); // Since the drawable is at the top of the view, we need to offset to keep it centered. - if (isVerticalBarLayout) { + if (isLandscape) { sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top); } else { sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale)); @@ -269,7 +268,7 @@ public class ClipIconView extends View implements ClipPathView { * Sets the icon for this view as part of initial setup */ public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp, - boolean isOpening, boolean isVerticalBarLayout, DeviceProfile dp) { + boolean isOpening, DeviceProfile dp) { mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable; if (mIsAdaptiveIcon) { boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon; @@ -304,7 +303,7 @@ public class ClipIconView extends View implements ClipPathView { Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale()); } - if (isVerticalBarLayout) { + if (dp.isLandscape) { lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio); } else { lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio); @@ -325,7 +324,7 @@ public class ClipIconView extends View implements ClipPathView { bgDrawableStartScale = scale; mOutline.set(0, 0, lp.width, lp.height); } - setBackgroundDrawableBounds(bgDrawableStartScale, isVerticalBarLayout); + setBackgroundDrawableBounds(bgDrawableStartScale, dp.isLandscape); mEndRevealRect.set(0, 0, lp.width, lp.height); setOutlineProvider(new ViewOutlineProvider() { @Override diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java index 0a800c3c6e..efc83ebcc9 100644 --- a/src/com/android/launcher3/views/FloatingIconView.java +++ b/src/com/android/launcher3/views/FloatingIconView.java @@ -17,7 +17,6 @@ package com.android.launcher3.views; import static com.android.launcher3.Utilities.getBadge; import static com.android.launcher3.Utilities.getFullDrawable; -import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; @@ -44,12 +43,12 @@ import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.dragndrop.DragLayer; -import com.android.launcher3.dragndrop.FolderAdaptiveIcon; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.icons.FastBitmapDrawable; @@ -59,6 +58,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.shortcuts.DeepShortcutView; +import java.util.function.Supplier; + /** * A view that is created to look like another view with the purpose of creating fluid animations. */ @@ -83,7 +84,6 @@ public class FloatingIconView extends FrameLayout implements private final Launcher mLauncher; private final boolean mIsRtl; - private boolean mIsVerticalBarLayout = false; private boolean mIsOpening; private IconLoadResult mIconLoadResult; @@ -151,7 +151,7 @@ public class FloatingIconView extends FrameLayout implements float shapeProgressStart, float cornerRadius, boolean isOpening) { setAlpha(alpha); mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, fgIconAlpha, - isOpening, this, mLauncher.getDeviceProfile(), mIsVerticalBarLayout); + isOpening, this, mLauncher.getDeviceProfile()); } @Override @@ -248,14 +248,15 @@ public class FloatingIconView extends FrameLayout implements * @param originalView The View that the FloatingIconView will replace. * @param info ItemInfo of the originalView * @param pos The position of the view. + * @param btvIcon The drawable of the BubbleTextView. May be null if original view is not a BTV + * @param outIconLoadResult We store the icon results into this object. */ @WorkerThread @SuppressWarnings("WrongThread") private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, - Drawable btvIcon, IconLoadResult iconLoadResult) { + @Nullable Drawable btvIcon, IconLoadResult outIconLoadResult) { Drawable drawable; - boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get() - && !info.isDisabled(); // Use original icon for disabled icons. + boolean supportsAdaptiveIcons = !info.isDisabled(); // Use original icon for disabled icons. Drawable badge = null; if (info instanceof SystemShortcut) { @@ -273,7 +274,9 @@ public class FloatingIconView extends FrameLayout implements int width = (int) pos.width(); int height = (int) pos.height(); if (supportsAdaptiveIcons) { - drawable = getFullDrawable(l, info, width, height, sTmpObjArray); + boolean shouldThemeIcon = btvIcon instanceof FastBitmapDrawable + && ((FastBitmapDrawable) btvIcon).isThemed(); + drawable = getFullDrawable(l, info, width, height, shouldThemeIcon, sTmpObjArray); if (drawable instanceof AdaptiveIconDrawable) { badge = getBadge(l, info, sTmpObjArray[0]); } else { @@ -286,24 +289,27 @@ public class FloatingIconView extends FrameLayout implements // Similar to DragView, we simply use the BubbleTextView icon here. drawable = btvIcon; } else { - drawable = getFullDrawable(l, info, width, height, sTmpObjArray); + drawable = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */, + sTmpObjArray); } } } drawable = drawable == null ? null : drawable.getConstantState().newDrawable(); int iconOffset = getOffsetForIconBounds(l, drawable, pos); - synchronized (iconLoadResult) { - iconLoadResult.btvDrawable = btvIcon == null || drawable == btvIcon - ? null : btvIcon.getConstantState().newDrawable(); - iconLoadResult.drawable = drawable; - iconLoadResult.badge = badge; - iconLoadResult.iconOffset = iconOffset; - if (iconLoadResult.onIconLoaded != null) { - l.getMainExecutor().execute(iconLoadResult.onIconLoaded); - iconLoadResult.onIconLoaded = null; + // Clone right away as we are on the background thread instead of blocking the + // main thread later + Drawable btvClone = btvIcon == null ? null : btvIcon.getConstantState().newDrawable(); + synchronized (outIconLoadResult) { + outIconLoadResult.btvDrawable = () -> btvClone; + outIconLoadResult.drawable = drawable; + outIconLoadResult.badge = badge; + outIconLoadResult.iconOffset = iconOffset; + if (outIconLoadResult.onIconLoaded != null) { + l.getMainExecutor().execute(outIconLoadResult.onIconLoaded); + outIconLoadResult.onIconLoaded = null; } - iconLoadResult.isIconLoaded = true; + outIconLoadResult.isIconLoaded = true; } } @@ -316,12 +322,12 @@ public class FloatingIconView extends FrameLayout implements */ @UiThread private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge, - @Nullable Drawable btvIcon, int iconOffset) { + @Nullable Supplier btvIcon, int iconOffset) { + final DeviceProfile dp = mLauncher.getDeviceProfile(); final InsettableFrameLayout.LayoutParams lp = (InsettableFrameLayout.LayoutParams) getLayoutParams(); mBadge = badge; - mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout, - mLauncher.getDeviceProfile()); + mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, dp); if (drawable instanceof AdaptiveIconDrawable) { final int originalHeight = lp.height; final int originalWidth = lp.width; @@ -329,7 +335,7 @@ public class FloatingIconView extends FrameLayout implements mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight); float aspectRatio = mLauncher.getDeviceProfile().aspectRatio; - if (mIsVerticalBarLayout) { + if (dp.isLandscape) { lp.width = (int) Math.max(lp.width, lp.height * aspectRatio); } else { lp.height = (int) Math.max(lp.height, lp.width * aspectRatio); @@ -337,15 +343,13 @@ public class FloatingIconView extends FrameLayout implements setLayoutParams(lp); final LayoutParams clipViewLp = (LayoutParams) mClipIconView.getLayoutParams(); - final int clipViewOgHeight = clipViewLp.height; - final int clipViewOgWidth = clipViewLp.width; + if (mBadge != null) { + Rect badgeBounds = new Rect(0, 0, clipViewLp.width, clipViewLp.height); + FastBitmapDrawable.setBadgeBounds(mBadge, badgeBounds); + } clipViewLp.width = lp.width; clipViewLp.height = lp.height; mClipIconView.setLayoutParams(clipViewLp); - - if (mBadge != null) { - mBadge.setBounds(0, 0, clipViewOgWidth, clipViewOgHeight); - } } setOriginalDrawableBackground(btvIcon); @@ -361,9 +365,9 @@ public class FloatingIconView extends FrameLayout implements * * Allows nullable as this may be cleared when drawing is deferred to ClipIconView. */ - private void setOriginalDrawableBackground(@Nullable Drawable btvIcon) { + private void setOriginalDrawableBackground(@Nullable Supplier btvIcon) { if (!mIsOpening) { - mBtvDrawable.setBackground(btvIcon); + mBtvDrawable.setBackground(btvIcon == null ? null : btvIcon.get()); } } @@ -412,8 +416,7 @@ public class FloatingIconView extends FrameLayout implements @WorkerThread @SuppressWarnings("WrongThread") private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) { - if (!(drawable instanceof AdaptiveIconDrawable) - || (drawable instanceof FolderAdaptiveIcon)) { + if (!(drawable instanceof AdaptiveIconDrawable)) { return 0; } int blurSizeOutline = @@ -519,22 +522,26 @@ public class FloatingIconView extends FrameLayout implements getLocationBoundsForView(l, v, isOpening, position); final FastBitmapDrawable btvIcon; + final Supplier btvDrawableSupplier; if (v instanceof BubbleTextView) { BubbleTextView btv = (BubbleTextView) v; if (info instanceof ItemInfoWithIcon && (((ItemInfoWithIcon) info).runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { btvIcon = btv.makePreloadIcon(); + btvDrawableSupplier = () -> btvIcon; } else { btvIcon = btv.getIcon(); + // Clone when needed + btvDrawableSupplier = () -> btvIcon.getConstantState().newDrawable(); } } else { btvIcon = null; + btvDrawableSupplier = null; } - IconLoadResult result = new IconLoadResult(info, - btvIcon == null ? false : btvIcon.isThemed()); - result.btvDrawable = btvIcon; + IconLoadResult result = new IconLoadResult(info, btvIcon != null && btvIcon.isThemed()); + result.btvDrawable = btvDrawableSupplier; final long fetchIconId = sFetchIconId++; MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> { @@ -565,7 +572,6 @@ public class FloatingIconView extends FrameLayout implements view.recycle(); // Init properties before getting the drawable. - view.mIsVerticalBarLayout = launcher.getDeviceProfile().isVerticalBarLayout(); view.mIsOpening = isOpening; view.mOriginalIcon = originalView; view.mPositionOut = positionOut; @@ -587,7 +593,7 @@ public class FloatingIconView extends FrameLayout implements view.matchPositionOf(launcher, originalView, isOpening, positionOut); // We need to add it to the overlay, but keep it invisible until animation starts.. - view.setVisibility(INVISIBLE); + setIconAndDotVisible(view, false); parent.addView(view); dragLayer.addView(view.mListenerView); view.mListenerView.setListener(view::fastFinish); @@ -596,16 +602,8 @@ public class FloatingIconView extends FrameLayout implements view.mEndRunnable = null; if (hideOriginal) { - if (isOpening) { - setIconAndDotVisible(originalView, true); - view.finish(dragLayer); - } else { - originalView.setVisibility(VISIBLE); - if (originalView instanceof IconLabelDotView) { - setIconAndDotVisible(originalView, true); - } - view.finish(dragLayer); - } + setIconAndDotVisible(originalView, true); + view.finish(dragLayer); } else { view.finish(dragLayer); } @@ -658,7 +656,7 @@ public class FloatingIconView extends FrameLayout implements private static class IconLoadResult { final ItemInfo itemInfo; final boolean isThemed; - Drawable btvDrawable; + Supplier btvDrawable; Drawable drawable; Drawable badge; int iconOffset; diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java index 09c8c64087..bfb75f0022 100644 --- a/src/com/android/launcher3/views/FloatingSurfaceView.java +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -40,8 +40,8 @@ import com.android.launcher3.GestureNavContract; import com.android.launcher3.Insettable; import com.android.launcher3.Launcher; import com.android.launcher3.R; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.window.RefreshRateTracker; /** * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes @@ -62,7 +62,6 @@ public class FloatingSurfaceView extends AbstractFloatingView implements private final SurfaceView mSurfaceView; - private View mIcon; private GestureNavContract mContract; @@ -97,13 +96,19 @@ public class FloatingSurfaceView extends AbstractFloatingView implements // Remove after some time, to avoid flickering Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable, - DisplayController.INSTANCE.get(mLauncher).getInfo().singleFrameMs); + RefreshRateTracker.getSingleFrameMs(mLauncher)); } private void removeViewFromParent() { mPicture.beginRecording(1, 1); mPicture.endRecording(); - mLauncher.getDragLayer().removeView(this); + mLauncher.getDragLayer().removeViewInLayout(this); + } + + private void removeViewImmediate() { + // Cancel any pending remove + Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(mRemoveViewRunnable); + removeViewFromParent(); } /** @@ -115,9 +120,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements view.mContract = contract; view.mIsOpen = true; - // Cancel any pending remove - Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable); - view.removeViewFromParent(); + view.removeViewImmediate(); launcher.getDragLayer().addView(view); } @@ -129,6 +132,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { close(false); + removeViewImmediate(); return false; } @@ -197,7 +201,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements private void sendIconInfo() { if (mContract != null && !mIconPosition.isEmpty()) { - mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl()); + mContract.sendEndPosition(mIconPosition, mLauncher, mSurfaceView.getSurfaceControl()); } } diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index fc8b4b7edb..2a9a8a5d36 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -180,18 +180,6 @@ public class OptionsPopupView extends ArrowPopup */ public static ArrayList getOptions(Launcher launcher) { ArrayList options = new ArrayList<>(); - options.add(new OptionItem(launcher, - R.string.settings_button_text, - R.drawable.ic_setting, - LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS, - OptionsPopupView::startSettings)); - if (!WidgetsModel.GO_DISABLE_WIDGETS) { - options.add(new OptionItem(launcher, - R.string.widget_button_text, - R.drawable.ic_widget, - LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS, - OptionsPopupView::onWidgetsClicked)); - } int resString = Utilities.existsStyleWallpapers(launcher) ? R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text; int resDrawable = Utilities.existsStyleWallpapers(launcher) ? @@ -201,6 +189,18 @@ public class OptionsPopupView extends ArrowPopup resDrawable, IGNORE, OptionsPopupView::startWallpaperPicker)); + if (!WidgetsModel.GO_DISABLE_WIDGETS) { + options.add(new OptionItem(launcher, + R.string.widget_button_text, + R.drawable.ic_widget, + LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS, + OptionsPopupView::onWidgetsClicked)); + } + options.add(new OptionItem(launcher, + R.string.settings_button_text, + R.drawable.ic_setting, + LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS, + OptionsPopupView::startSettings)); return options; } @@ -229,7 +229,7 @@ public class OptionsPopupView extends ArrowPopup Launcher launcher = Launcher.getLauncher(view.getContext()); launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES) .setPackage(launcher.getPackageName()) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); return true; } @@ -240,7 +240,10 @@ public class OptionsPopupView extends ArrowPopup private static boolean startWallpaperPicker(View v) { Launcher launcher = Launcher.getLauncher(v.getContext()); if (!Utilities.isWallpaperAllowed(launcher)) { - Toast.makeText(launcher, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show(); + String message = launcher.getStringCache() != null + ? launcher.getStringCache().disabledByAdminMessage + : launcher.getString(R.string.msg_disabled_by_admin); + Toast.makeText(launcher, message, Toast.LENGTH_SHORT).show(); return false; } Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER) diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java index a982786972..11ca130179 100644 --- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -20,6 +20,8 @@ import static android.view.HapticFeedbackConstants.CLOCK_TICK; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; +import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync; + import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; @@ -43,7 +45,7 @@ import android.widget.TextView; import androidx.annotation.RequiresApi; import androidx.recyclerview.widget.RecyclerView; -import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.FastScrollRecyclerView; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.graphics.FastScrollThumbDrawable; @@ -127,7 +129,7 @@ public class RecyclerViewFastScroller extends View { private String mPopupSectionName; private Insets mSystemGestureInsets; - protected BaseRecyclerView mRv; + protected FastScrollRecyclerView mRv; private RecyclerView.OnScrollListener mOnScrollListener; private int mDownX; @@ -172,7 +174,7 @@ public class RecyclerViewFastScroller extends View { ta.recycle(); } - public void setRecyclerView(BaseRecyclerView rv, TextView popupView) { + public void setRecyclerView(FastScrollRecyclerView rv, TextView popupView) { if (mRv != null && mOnScrollListener != null) { mRv.removeOnScrollListener(mOnScrollListener); } @@ -306,6 +308,7 @@ public class RecyclerViewFastScroller extends View { } private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { + hideKeyboardAsync(ActivityContext.lookupContext(getContext()), getWindowToken()); mIsDragging = true; if (mCanThumbDetach) { mIsThumbDetached = true; diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java index d2d569f79f..9442734646 100644 --- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java @@ -18,7 +18,6 @@ package com.android.launcher3.widget; import static com.android.launcher3.Utilities.ATLEAST_R; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; -import static com.android.launcher3.widget.BaseWidgetSheet.MAX_WIDTH_SCALE_FOR_LARGER_SCREEN; import android.animation.PropertyValuesHolder; import android.annotation.SuppressLint; @@ -106,7 +105,10 @@ public class AddItemWidgetsBottomSheet extends AbstractSlideInView 0) { + if (deviceProfile.isTablet) { + int margin = deviceProfile.allAppsLeftRightMargin; + widthUsed = Math.max(2 * margin, 2 * (mInsets.left + mInsets.right)); + } else if (mInsets.bottom > 0) { widthUsed = mInsets.left + mInsets.right; } else { Rect padding = deviceProfile.workspacePadding; @@ -114,18 +116,8 @@ public class AddItemWidgetsBottomSheet extends AbstractSlideInView PopupDataProvider.PopupDataChangeListener, Insettable { /** The default number of cells that can fit horizontally in a widget sheet. */ protected static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4; - /** - * The maximum scale, [0, 1], of the device screen width that the widgets picker can consume - * on large screen devices. - */ - protected static final float MAX_WIDTH_SCALE_FOR_LARGER_SCREEN = 0.89f; protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN = "launcher.widgets_education_tip_seen"; @@ -69,10 +69,15 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView private int mContentHorizontalMarginInPx; + protected int mNavBarScrimHeight; + private final Paint mNavBarScrimPaint; + public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContentHorizontalMarginInPx = getResources().getDimensionPixelSize( R.dimen.widget_list_horizontal_margin); + mNavBarScrimPaint = new Paint(); + mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); } protected int getScrimColor(Context context) { @@ -82,6 +87,9 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext()) + .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect()); + mNavBarScrimHeight = getNavBarScrimHeight(windowInsets); mActivityContext.getPopupDataProvider().setChangeListener(this); } @@ -135,6 +143,30 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView } } + private int getNavBarScrimHeight(WindowInsets insets) { + if (Utilities.ATLEAST_Q) { + return insets.getTappableElementInsets().bottom; + } else { + return insets.getStableInsetBottom(); + } + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mNavBarScrimHeight = getNavBarScrimHeight(insets); + return super.onApplyWindowInsets(insets); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mNavBarScrimHeight > 0) { + canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), + mNavBarScrimPaint); + } + } + /** Called when the horizontal margin of the content view has changed. */ protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx); @@ -146,7 +178,10 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int widthUsed; - if (mInsets.bottom > 0) { + if (deviceProfile.isTablet) { + int margin = deviceProfile.allAppsLeftRightMargin; + widthUsed = Math.max(2 * margin, 2 * (mInsets.left + mInsets.right)); + } else if (mInsets.bottom > 0) { widthUsed = mInsets.left + mInsets.right; } else { Rect padding = deviceProfile.workspacePadding; @@ -154,18 +189,8 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView 2 * (mInsets.left + mInsets.right)); } - if (deviceProfile.isTablet || deviceProfile.isTwoPanels) { - // In large screen devices, we restrict the width of the widgets picker to show part of - // the home screen. Let's ensure the minimum width used is at least the minimum width - // that isn't taken by the widgets picker. - int minUsedWidth = (int) (deviceProfile.availableWidthPx - * (1 - MAX_WIDTH_SCALE_FOR_LARGER_SCREEN)); - widthUsed = Math.max(widthUsed, minUsedWidth); - } - - int heightUsed = mInsets.top + deviceProfile.edgeMarginPx; measureChildWithMargins(mContent, widthMeasureSpec, - widthUsed, heightMeasureSpec, heightUsed); + widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); } @@ -173,7 +198,9 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView /** Returns the number of cells that can fit horizontally in a given {@code content}. */ protected int computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx) { DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); - int availableWidth = content.getMeasuredWidth() - contentHorizontalPaddingPx; + int availableWidth = content.getMeasuredWidth() + - contentHorizontalPaddingPx + - (2 * mContentHorizontalMarginInPx); Point cellSize = deviceProfile.getCellSize(); if (cellSize.x > 0) { return availableWidth / cellSize.x; @@ -307,4 +334,11 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView return mActivityContext.getSharedPrefs().getBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, false) || Utilities.IS_RUNNING_IN_TEST_HARNESS; } + + @Override + protected void setTranslationShift(float translationShift) { + super.setTranslationShift(translationShift); + Launcher launcher = ActivityContext.lookupContext(getContext()); + launcher.onWidgetsTransition(1 - translationShift); + } } diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java index 784f4f050e..7030f6dede 100644 --- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java +++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java @@ -25,15 +25,10 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Handler; -import android.os.Process; -import android.os.UserHandle; -import android.util.ArrayMap; import android.util.Log; import android.util.Size; @@ -44,7 +39,6 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.icons.BitmapRenderer; -import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.icons.ShadowGenerator; import com.android.launcher3.icons.cache.HandlerRunnable; @@ -65,9 +59,6 @@ public class DatabaseWidgetPreviewLoader { private final Context mContext; private final float mPreviewBoxCornerRadius; - private final UserHandle mMyUser = Process.myUserHandle(); - private final ArrayMap mUserBadges = new ArrayMap<>(); - public DatabaseWidgetPreviewLoader(Context context) { mContext = context; float previewCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context); @@ -108,52 +99,6 @@ public class DatabaseWidgetPreviewLoader { } } - /** - * Returns a drawable that can be used as a badge for the user or null. - */ - // @UiThread - public Drawable getBadgeForUser(UserHandle user, int badgeSize) { - if (mMyUser.equals(user)) { - return null; - } - - Bitmap badgeBitmap = getUserBadge(user, badgeSize); - FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap); - d.setFilterBitmap(true); - d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight()); - return d; - } - - private Bitmap getUserBadge(UserHandle user, int badgeSize) { - synchronized (mUserBadges) { - Bitmap badgeBitmap = mUserBadges.get(user); - if (badgeBitmap != null) { - return badgeBitmap; - } - - final Resources res = mContext.getResources(); - badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888); - - Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity( - new BitmapDrawable(res, badgeBitmap), user, - new Rect(0, 0, badgeSize, badgeSize), - 0); - if (drawable instanceof BitmapDrawable) { - badgeBitmap = ((BitmapDrawable) drawable).getBitmap(); - } else { - badgeBitmap.eraseColor(Color.TRANSPARENT); - Canvas c = new Canvas(badgeBitmap); - drawable.setBounds(0, 0, badgeSize, badgeSize); - drawable.draw(c); - c.setBitmap(null); - } - - mUserBadges.put(user, badgeBitmap); - return badgeBitmap; - } - } - - /** * Generates the widget preview from either the {@link WidgetManagerHelper} or cache * and add badge at the bottom right corner. @@ -318,8 +263,8 @@ public class DatabaseWidgetPreviewLoader { LauncherIcons li = LauncherIcons.obtain(mContext); Drawable icon = li.createBadgedIconBitmap( mutateOnMainThread(info.getFullResIcon( - LauncherAppState.getInstance(mContext).getIconCache())), - Process.myUserHandle(), 0).newIcon(mContext); + LauncherAppState.getInstance(mContext).getIconCache()))) + .newIcon(mContext); li.recycle(); icon.setBounds(padding, padding, padding + iconSize, padding + iconSize); diff --git a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java index 57f8bc7717..f42142ecf0 100644 --- a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java @@ -16,7 +16,6 @@ package com.android.launcher3.widget; -import android.annotation.SuppressLint; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.graphics.Canvas; @@ -31,9 +30,6 @@ import android.widget.RemoteViews; import com.android.launcher3.R; -import java.io.PrintWriter; -import java.io.StringWriter; - /** * A widget host views created while the host has not bind to the system service. */ @@ -73,34 +69,23 @@ public class DeferredAppWidgetHostView extends LauncherAppWidgetHostView { return; } - // Use double padding so that there is extra space between background and text + // Use double padding so that there is extra space between background and text if possible. int availableWidth = getMeasuredWidth() - 2 * (getPaddingLeft() + getPaddingRight()); + if (availableWidth <= 0) { + availableWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight()); + } if (mSetupTextLayout != null && mSetupTextLayout.getText().equals(info.label) && mSetupTextLayout.getWidth() == availableWidth) { return; } - try { - mSetupTextLayout = new StaticLayout(info.label, mPaint, availableWidth, - Layout.Alignment.ALIGN_CENTER, 1, 0, true); - } catch (IllegalArgumentException e) { - @SuppressLint("DrawAllocation") StringWriter stringWriter = new StringWriter(); - @SuppressLint("DrawAllocation") PrintWriter printWriter = new PrintWriter(stringWriter); - mActivity.getDeviceProfile().dump(/*prefix=*/"", printWriter); - printWriter.flush(); - String message = "b/203530620 " - + "- availableWidth: " + availableWidth - + ", getMeasuredWidth: " + getMeasuredWidth() - + ", getPaddingLeft: " + getPaddingLeft() - + ", getPaddingRight: " + getPaddingRight() - + ", deviceProfile: " + stringWriter.toString(); - throw new IllegalArgumentException(message, e); - } + mSetupTextLayout = new StaticLayout(info.label, mPaint, availableWidth, + Layout.Alignment.ALIGN_CENTER, 1, 0, true); } @Override protected void onDraw(Canvas canvas) { if (mSetupTextLayout != null) { - canvas.translate(getPaddingLeft() * 2, + canvas.translate((getWidth() - mSetupTextLayout.getWidth()) / 2, (getHeight() - mSetupTextLayout.getHeight()) / 2); mSetupTextLayout.draw(canvas); } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index f0b4ba0252..08651523a6 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -16,12 +16,16 @@ package com.android.launcher3.widget; +import android.annotation.TargetApi; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Build; import android.os.Handler; import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.MotionEvent; @@ -52,6 +56,8 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView implements TouchCompleteListener, View.OnLongClickListener, LocalColorExtractor.Listener { + private static final String TAG = "LauncherAppWidgetHostView"; + // Related to the auto-advancing of widgets private static final long ADVANCE_INTERVAL = 20000; private static final long ADVANCE_STAGGER = 250; @@ -61,6 +67,8 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView // Maximum duration for which updates can be deferred. private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000; + private static final String TRACE_METHOD_NAME = "appwidget load-widget "; + private final Rect mTempRect = new Rect(); private final CheckLongPressHelper mLongPressHelper; protected final Launcher mLauncher; @@ -88,6 +96,8 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView /** The drag content height which is only set when the drag content scale is not 1f. */ private int mDragContentHeight = 0; + private boolean mTrackingWidgetUpdate = false; + public LauncherAppWidgetHostView(Context context) { super(context); mLauncher = Launcher.getLauncher(context); @@ -121,7 +131,25 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView } @Override + @TargetApi(Build.VERSION_CODES.Q) + public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { + super.setAppWidget(appWidgetId, info); + if (!mTrackingWidgetUpdate && Utilities.ATLEAST_Q) { + mTrackingWidgetUpdate = true; + Trace.beginAsyncSection(TRACE_METHOD_NAME + info.provider, appWidgetId); + Log.i(TAG, "App widget created with id: " + appWidgetId); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.Q) public void updateAppWidget(RemoteViews remoteViews) { + if (mTrackingWidgetUpdate && remoteViews != null && Utilities.ATLEAST_Q) { + Log.i(TAG, "App widget with id: " + getAppWidgetId() + " loaded"); + Trace.endAsyncSection( + TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId()); + mTrackingWidgetUpdate = false; + } if (isDeferringUpdates()) { mDeferredRemoteViews = remoteViews; return; @@ -429,7 +457,8 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView } // Remove and rebind the current widget (which was inflated in the wrong // orientation), but don't delete it from the database - mLauncher.removeItem(this, info, false /* deleteFromDb */); + mLauncher.removeItem(this, info, false /* deleteFromDb */, + "widget removed because of configuration change"); mLauncher.bindAppWidget(info); } diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index cbec6427a6..470a800366 100644 --- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -70,7 +70,7 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo { public LauncherAtom.ItemInfo buildProto(FolderInfo folderInfo) { LauncherAtom.ItemInfo info = super.buildProto(folderInfo); return info.toBuilder() - .setAttribute(LauncherAppWidgetInfo.getAttribute(sourceContainer)) + .addItemAttributes(LauncherAppWidgetInfo.getAttribute(sourceContainer)) .build(); } } diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java index 553ba13fe7..130ee3a70c 100644 --- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java @@ -17,7 +17,7 @@ package com.android.launcher3.widget; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; -import static com.android.launcher3.widget.WidgetSections.getWidgetSections; +import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; import android.content.Context; import android.graphics.Canvas; @@ -159,8 +159,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView disabledIcon.setIsDisabled(true); mCenterDrawable = disabledIcon; } else { - widgetCategoryIcon.setColorFilter( - FastBitmapDrawable.getDisabledFColorFilter(/* disabledAlpha= */ 1f)); + widgetCategoryIcon.setColorFilter(getDisabledColorFilter()); mCenterDrawable = widgetCategoryIcon; } mSettingIconDrawable = null; @@ -341,8 +340,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView if (mInfo.pendingItemInfo.widgetCategory == WidgetSections.NO_CATEGORY) { return null; } - Context context = getContext(); - return context.getDrawable(getWidgetSections(context).get( - mInfo.pendingItemInfo.widgetCategory).mSectionDrawable); + return mInfo.pendingItemInfo.newIcon(getContext()); } } diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java index 463f4ac1b1..46c0b99938 100644 --- a/src/com/android/launcher3/widget/PendingItemDragHelper.java +++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java @@ -181,8 +181,7 @@ public class PendingItemDragHelper extends DragPreviewProvider { PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) mAddInfo; Drawable icon = createShortcutInfo.activityInfo.getFullResIcon(app.getIconCache()); LauncherIcons li = LauncherIcons.obtain(launcher); - preview = new FastBitmapDrawable( - li.createScaledBitmapWithoutShadow(icon, 0)); + preview = new FastBitmapDrawable(li.createScaledBitmapWithoutShadow(icon)); previewWidth = preview.getIntrinsicWidth(); previewHeight = preview.getIntrinsicHeight(); li.recycle(); diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index c92fe5a516..2796721c63 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -26,6 +26,7 @@ import static com.android.launcher3.Utilities.ATLEAST_S; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.os.Process; import android.util.AttributeSet; import android.util.Log; import android.util.Size; @@ -48,7 +49,6 @@ import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; -import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.RoundDrawableWrapper; import com.android.launcher3.icons.cache.HandlerRunnable; @@ -372,14 +372,11 @@ public class WidgetCell extends LinearLayout { /** Used to show the badge when the widget is in the recommended section */ public void showBadge() { - Drawable badge = mWidgetPreviewLoader.getBadgeForUser(mItem.user, - BaseIconFactory.getBadgeSizeForIconSize( - mActivity.getDeviceProfile().allAppsIconSizePx)); - if (badge == null) { + if (Process.myUserHandle().equals(mItem.user)) { mWidgetBadge.setVisibility(View.GONE); } else { mWidgetBadge.setVisibility(View.VISIBLE); - mWidgetBadge.setImageDrawable(badge); + mWidgetBadge.setImageResource(R.drawable.ic_work_app_badge); } } diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index b152ddc2d9..bf521cc7eb 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -247,10 +247,12 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { @Override public void setInsets(Rect insets) { super.setInsets(insets); + int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); mContent.setPadding(mContent.getPaddingStart(), - mContent.getPaddingTop(), mContent.getPaddingEnd(), insets.bottom); - if (insets.bottom > 0) { + mContent.getPaddingTop(), mContent.getPaddingEnd(), + bottomPadding); + if (bottomPadding > 0) { setupNavBarColor(); } else { clearNavBarColor(); diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 894c4c9cd3..a49cdc005a 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -31,6 +31,7 @@ import android.content.res.Resources; import android.graphics.Rect; import android.os.Process; import android.os.UserHandle; +import android.os.UserManager; import android.util.AttributeSet; import android.util.Pair; import android.util.SparseArray; @@ -41,6 +42,7 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; +import android.widget.Button; import android.widget.TextView; import androidx.annotation.Nullable; @@ -54,7 +56,9 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.model.UserManagerState; import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.views.ArrowTipView; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.SpringRelativeLayout; @@ -92,14 +96,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN = "launcher.widgets_education_dialog_seen"; - private final Rect mInsets = new Rect(); + private final UserManagerState mUserManagerState = new UserManagerState(); + private final boolean mHasWorkProfile; private final SparseArray mAdapters = new SparseArray(); private final UserHandle mCurrentUser = Process.myUserHandle(); private final Predicate mPrimaryWidgetsFilter = entry -> mCurrentUser.equals(entry.mPkgItem.user); private final Predicate mWorkWidgetsFilter = - mPrimaryWidgetsFilter.negate(); + entry -> !mCurrentUser.equals(entry.mPkgItem.user) + && !mUserManagerState.isUserQuiet(entry.mPkgItem.user); @Nullable private ArrowTipView mLatestEducationalTip; private final OnLayoutChangeListener mLayoutChangeListenerToShowTips = new OnLayoutChangeListener() { @@ -170,6 +176,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet : 0; mWidgetSheetContentHorizontalPadding = 2 * resources.getDimensionPixelSize( R.dimen.widget_cell_horizontal_padding); + + mUserManagerState.init(UserCache.INSTANCE.get(context), + context.getSystemService(UserManager.class)); } public WidgetsFullSheet(Context context, AttributeSet attrs) { @@ -199,6 +208,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet findViewById(R.id.tab_work) .setOnClickListener((View view) -> mViewPager.snapToPage(1)); mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); + setDeviceManagementResources(); } else { mViewPager = null; } @@ -220,6 +230,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet setUpEducationViewsIfNeeded(); } + private void setDeviceManagementResources() { + if (mActivityContext.getStringCache() != null) { + Button personalTab = findViewById(R.id.tab_personal); + personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab); + + Button workTab = findViewById(R.id.tab_work); + workTab.setText(mActivityContext.getStringCache().widgetsWorkTab); + } + } + @Override public void onActivePageChanged(int currentActivePage) { AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); @@ -247,10 +267,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); - mNoWidgetsView.setText( - adapterHolder.mAdapterType == AdapterHolder.SEARCH - ? R.string.no_search_results - : R.string.no_widgets_available); + if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) { + mNoWidgetsView.setText(R.string.no_search_results); + } else if (adapterHolder.mAdapterType == AdapterHolder.WORK + && mUserManagerState.isAnyProfileQuietModeEnabled() + && mActivityContext.getStringCache() != null) { + mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle); + } else { + mNoWidgetsView.setText(R.string.no_widgets_available); + } mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE); } @@ -303,15 +328,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override public void setInsets(Rect insets) { super.setInsets(insets); - - setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom); - setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, insets.bottom); + int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); + setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, bottomPadding); + setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, bottomPadding); if (mHasWorkProfile) { - setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom); + setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, bottomPadding); } - ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = insets.bottom; + ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = bottomPadding; - if (insets.bottom > 0) { + if (bottomPadding > 0) { setupNavBarColor(); } else { clearNavBarColor(); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java index 932e06d57b..48df04fc85 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.widget.picker; -import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY; import android.content.Context; import android.content.res.Resources; @@ -43,8 +42,6 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.util.PluralMessageFormat; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.widget.WidgetSections; -import com.android.launcher3.widget.WidgetSections.WidgetSection; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; @@ -98,7 +95,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd mTitle = findViewById(R.id.app_title); mSubtitle = findViewById(R.id.app_subtitle); mExpandToggle = findViewById(R.id.toggle); - findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() { + setAccessibilityDelegate(new AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { @@ -177,13 +174,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd private void setIcon(PackageItemInfo info) { Drawable icon; - if (info.widgetCategory == NO_CATEGORY) { - icon = info.newIcon(getContext()); - } else { - WidgetSection widgetSection = WidgetSections.getWidgetSections(getContext()) - .get(info.widgetCategory); - icon = getContext().getDrawable(widgetSection.mSectionDrawable); - } + icon = info.newIcon(getContext()); applyDrawables(icon); mIconDrawable = icon; if (mIconDrawable != null) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java index f780f03948..bdf646bfaf 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java @@ -27,8 +27,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; -import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.FastScrollRecyclerView; import com.android.launcher3.R; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.model.WidgetListSpaceEntry; @@ -41,7 +41,7 @@ import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpa /** * The widgets recycler view. */ -public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouchListener { +public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnItemTouchListener { private WidgetsListAdapter mAdapter; @@ -239,21 +239,6 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch mHeaderViewDimensionsProvider = headerViewDimensionsProvider; } - @Override - public void scrollToTop() { - if (mScrollbar != null) { - mScrollbar.reattachThumbToScroll(); - } - - if (getLayoutManager() instanceof LinearLayoutManager) { - if (getCurrentScrollY() == 0) { - // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). - return; - } - } - scrollToPosition(0); - } - /** * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until * {@code untilIndex}. diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java index 2751a52796..a15508a617 100644 --- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java +++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java @@ -94,11 +94,6 @@ public class WidgetsSearchBarController implements TextWatcher, mSearchModeListener.onSearchResults(items); } - @Override - public void onAppendSearchResult(String query, ArrayList items) { - // Not needed. - } - @Override public void clearSearchResult() { // Any existing search session will be cancelled by setting text to empty. diff --git a/src_build_config/com/android/launcher3/BuildConfig.java b/src_build_config/com/android/launcher3/BuildConfig.java index 49aadf61ff..9a81d3f54c 100644 --- a/src_build_config/com/android/launcher3/BuildConfig.java +++ b/src_build_config/com/android/launcher3/BuildConfig.java @@ -17,6 +17,11 @@ package com.android.launcher3; public final class BuildConfig { - public static final String APPLICATION_ID = "com.android.launcher3"; - public static final boolean DEBUG = false; + public static final String APPLICATION_ID = "com.android.launcher3"; + public static final boolean DEBUG = false; + /** + * Flag to state if the QSB is on the first screen and placed on the top, + * this can be overwritten in other launchers with a different value, if needed. + */ + public static final boolean QSB_ON_FIRST_SCREEN = true; } diff --git a/src_plugins/com/android/systemui/plugins/OverlayPlugin.java b/src_plugins/com/android/systemui/plugins/LauncherOverlayPlugin.java similarity index 88% rename from src_plugins/com/android/systemui/plugins/OverlayPlugin.java rename to src_plugins/com/android/systemui/plugins/LauncherOverlayPlugin.java index 1edb69273f..9e223556bc 100644 --- a/src_plugins/com/android/systemui/plugins/OverlayPlugin.java +++ b/src_plugins/com/android/systemui/plugins/LauncherOverlayPlugin.java @@ -24,8 +24,8 @@ import com.android.systemui.plugins.shared.LauncherOverlayManager; /** * Implement this interface to add a -1 content on the home screen. */ -@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION) -public interface OverlayPlugin extends Plugin { +@ProvidesInterface(action = LauncherOverlayPlugin.ACTION, version = LauncherOverlayPlugin.VERSION) +public interface LauncherOverlayPlugin extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERLAY"; int VERSION = 1; diff --git a/src_plugins/com/android/systemui/plugins/OneSearch.java b/src_plugins/com/android/systemui/plugins/OneSearch.java index 13a956bd80..534bc87df4 100644 --- a/src_plugins/com/android/systemui/plugins/OneSearch.java +++ b/src_plugins/com/android/systemui/plugins/OneSearch.java @@ -28,7 +28,7 @@ import java.util.ArrayList; @ProvidesInterface(action = OneSearch.ACTION, version = OneSearch.VERSION) public interface OneSearch extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_ONE_SEARCH"; - int VERSION = 5; + int VERSION = 6; /** * Get the content provider warmed up. @@ -45,6 +45,8 @@ public interface OneSearch extends Plugin { /** Get image bitmap with the URL. */ Parcelable getImageBitmap(String imageUrl); + void setSuggestOnChrome(boolean enable); + /** * Notifies search events to plugin * diff --git a/src_shortcuts_overrides/com/android/launcher3/util/AbsGridOccupancy.java b/src_shortcuts_overrides/com/android/launcher3/util/AbsGridOccupancy.java new file mode 100644 index 0000000000..968b281787 --- /dev/null +++ b/src_shortcuts_overrides/com/android/launcher3/util/AbsGridOccupancy.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +/** + * Defines method to find the next vacant cell on a grid. + * This uses the default top-down, left-right approach and can be over-written through + * code swaps in different launchers. + */ +public abstract class AbsGridOccupancy { + /** + * Find the first vacant cell, if there is one. + * + * @param vacantOut Holds the x and y coordinate of the vacant cell + * @param spanX Horizontal cell span. + * @param spanY Vertical cell span. + * + * @return true if a vacant cell was found + */ + protected boolean findVacantCell(int[] vacantOut, boolean[][] cells, int countX, int countY, + int spanX, int spanY) { + for (int y = 0; (y + spanY) <= countY; y++) { + for (int x = 0; (x + spanX) <= countX; x++) { + boolean available = !cells[x][y]; + out: + for (int i = x; i < x + spanX; i++) { + for (int j = y; j < y + spanY; j++) { + available = available && !cells[i][j]; + if (!available) break out; + } + } + if (available) { + vacantOut[0] = x; + vacantOut[1] = y; + return true; + } + } + } + return false; + } +} diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java index 81e3f988ba..67157494d8 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java @@ -19,7 +19,6 @@ package com.android.launcher3.uioverrides; import android.app.Person; import android.content.Context; import android.content.pm.ShortcutInfo; -import android.view.Display; import com.android.launcher3.Utilities; @@ -31,20 +30,6 @@ public class ApiWrapper { return Utilities.EMPTY_PERSON_ARRAY; } - /** - * Returns true if the display is an internal displays - */ - public static boolean isInternalDisplay(Display display) { - return display.getDisplayId() == Display.DEFAULT_DISPLAY; - } - - /** - * Returns a unique ID representing the display - */ - public static String getUniqueId(Display display) { - return Integer.toString(display.getDisplayId()); - } - /** * Returns the minimum space that should be left empty at the end of hotseat */ diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java index 978c321cb4..bf35dd8d08 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -20,6 +20,7 @@ import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAP import android.content.Context; +import com.android.launcher3.DeviceProfile.DeviceProfileListenable; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; @@ -31,23 +32,20 @@ import com.android.launcher3.util.Themes; public class AllAppsState extends LauncherState { private static final float PARALLAX_COEFFICIENT = .125f; + private static final float WORKSPACE_SCALE_FACTOR = 0.97f; private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE; - private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { - @Override - public float getPageAlpha(int pageIndex) { - return 0; - } - }; - public AllAppsState(int id) { super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS); } @Override - public int getTransitionDuration(Context context) { - return 320; + public + int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState) { + return !context.getDeviceProfile().isTablet && isToState + ? 600 + : isToState ? 500 : 300; } @Override @@ -62,13 +60,34 @@ public class AllAppsState extends LauncherState { @Override public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) { - return new ScaleAndTranslation(1f, 0, - -launcher.getAllAppsController().getShiftRange() * PARALLAX_COEFFICIENT); + return new ScaleAndTranslation(WORKSPACE_SCALE_FACTOR, NO_OFFSET, NO_OFFSET); + } + + @Override + public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) { + if (launcher.getDeviceProfile().isTablet) { + return getWorkspaceScaleAndTranslation(launcher); + } else { + ScaleAndTranslation overviewScaleAndTranslation = LauncherState.OVERVIEW + .getWorkspaceScaleAndTranslation(launcher); + return new ScaleAndTranslation( + WORKSPACE_SCALE_FACTOR, + overviewScaleAndTranslation.translationX, + overviewScaleAndTranslation.translationY); + } } @Override public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { - return PAGE_ALPHA_PROVIDER; + PageAlphaProvider superPageAlphaProvider = super.getWorkspacePageAlphaProvider(launcher); + return new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return launcher.getDeviceProfile().isTablet + ? superPageAlphaProvider.getPageAlpha(pageIndex) + : 0; + } + }; } @Override @@ -78,6 +97,8 @@ public class AllAppsState extends LauncherState { @Override public int getWorkspaceScrimColor(Launcher launcher) { - return Themes.getAttrColor(launcher, R.attr.allAppsScrimColor); + return launcher.getDeviceProfile().isTablet + ? launcher.getResources().getColor(R.color.widgets_picker_scrim) + : Themes.getAttrColor(launcher, R.attr.allAppsScrimColor); } } diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java index d1543175f4..7a228c42b8 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java @@ -34,7 +34,7 @@ public class OverviewState extends LauncherState { } @Override - public int getTransitionDuration(Context context) { + public int getTransitionDuration(Context context, boolean isToState) { return 250; } diff --git a/tests/Android.bp b/tests/Android.bp index 4f6e8a3390..54cded0fd3 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -11,6 +11,7 @@ // 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 { // See: http://go/android-license-faq default_applicable_licenses: ["Android-Apache-2.0"], @@ -19,7 +20,10 @@ package { // Source code used for test filegroup { name: "launcher-tests-src", - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt" + ], } // Source code used for oop test helpers @@ -28,16 +32,19 @@ filegroup { srcs: [ "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", "src/com/android/launcher3/ui/PortraitLandscapeRunner.java", + "src/com/android/launcher3/util/TestUtil.java", "src/com/android/launcher3/util/Wait.java", "src/com/android/launcher3/util/WidgetUtils.java", "src/com/android/launcher3/util/rule/FailureWatcher.java", "src/com/android/launcher3/util/rule/LauncherActivityRule.java", + "src/com/android/launcher3/util/rule/SamplerRule.java", "src/com/android/launcher3/util/rule/ScreenRecordRule.java", "src/com/android/launcher3/util/rule/ShellCommandRule.java", "src/com/android/launcher3/util/rule/SimpleActivityRule.java", "src/com/android/launcher3/util/rule/TestStabilityRule.java", "src/com/android/launcher3/ui/TaplTestsLauncher3.java", "src/com/android/launcher3/testcomponent/BaseTestingActivity.java", + "src/com/android/launcher3/testcomponent/OtherBaseTestingActivity.java", "src/com/android/launcher3/testcomponent/CustomShortcutConfigActivity.java", "src/com/android/launcher3/testcomponent/TestCommandReceiver.java", "src/com/android/launcher3/testcomponent/TestLauncherActivity.java", @@ -61,12 +68,18 @@ android_library { "androidx.test.uiautomator_uiautomator", "mockito-target-inline-minus-junit4", "launcher_log_protos_lite", - "truth-prebuilt" + "truth-prebuilt", + "platform-test-rules", ], manifest: "AndroidManifest-common.xml", platform_apis: true, } +android_library { + name: "Launcher3TestResources", + resource_dirs: ["res"], +} + android_test { name: "Launcher3Tests", srcs: [ diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index e44c172081..9cc3aeda1f 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -94,7 +94,6 @@ - @@ -138,6 +137,7 @@ + @@ -267,6 +267,15 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/res/raw/devices.json b/tests/res/raw/devices.json new file mode 100644 index 0000000000..a78dd86464 --- /dev/null +++ b/tests/res/raw/devices.json @@ -0,0 +1,45 @@ +{ + "pixel6pro": { + "width": 1440, + "height": 3120, + "density": 560, + "name": "pixel6pro", + "cutout": "0, 130, 0, 0", + "grids": [ + "normal", + "reasonable", + "practical", + "big", + "crazy_big" + ], + "resourceOverrides": { + "status_bar_height": 98, + "navigation_bar_height_landscape": 56, + "navigation_bar_height": 56, + "navigation_bar_width": 56 + } + }, + "test": { + "data needs updating": 0 + }, + "pixel5": { + "width": 1080, + "height": 2340, + "density": 440, + "name": "pixel5", + "cutout": "0, 136, 0, 0", + "grids": [ + "normal", + "reasonable", + "practical", + "big", + "crazy_big" + ], + "resourceOverrides": { + "status_bar_height": 66, + "navigation_bar_height_landscape": 44, + "navigation_bar_height": 44, + "navigation_bar_width": 44 + } + } +} diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt new file mode 100644 index 0000000000..6d0fcb6faa --- /dev/null +++ b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3 + +import android.content.Context +import android.graphics.PointF +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.util.DisplayController.Info +import com.android.launcher3.util.WindowBounds +import org.junit.Before +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` as whenever + +abstract class DeviceProfileBaseTest { + + protected var context: Context? = null + protected var inv: InvariantDeviceProfile? = null + protected var info: Info = mock(Info::class.java) + protected var windowBounds: WindowBounds? = null + protected var isMultiWindowMode: Boolean = false + protected var transposeLayoutWithOrientation: Boolean = false + protected var useTwoPanels: Boolean = false + protected var isGestureMode: Boolean = true + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + // make sure to reset values + useTwoPanels = false + isGestureMode = true + } + + protected fun newDP(): DeviceProfile = DeviceProfile( + context, + inv, + info, + windowBounds, + isMultiWindowMode, + transposeLayoutWithOrientation, + useTwoPanels, + isGestureMode + ) + + protected fun initializeVarsForPhone(isLandscape: Boolean = false) { + val (x, y) = if (isLandscape) + Pair(3120, 1440) + else + Pair(1440, 3120) + + windowBounds = WindowBounds(x, y, x, y - 100, 0) + + whenever(info.isTablet(any())).thenReturn(false) + whenever(info.getDensityDpi()).thenReturn(560) + + inv = newScalableInvariantDeviceProfile() + } + + protected fun initializeVarsForTablet(isLandscape: Boolean = false) { + val (x, y) = if (isLandscape) + Pair(2560, 1600) + else + Pair(1600, 2560) + + windowBounds = WindowBounds(x, y, x, y - 100, 0) + + whenever(info.isTablet(any())).thenReturn(true) + whenever(info.getDensityDpi()).thenReturn(320) + + inv = newScalableInvariantDeviceProfile() + } + + /** + * A very generic grid, just to make qsb tests work. For real calculations, make sure to use + * values that better represent a real grid. + */ + protected fun newScalableInvariantDeviceProfile(): InvariantDeviceProfile = + InvariantDeviceProfile().apply { + isScalable = true + numColumns = 4 + numRows = 4 + numShownHotseatIcons = 4 + numDatabaseHotseatIcons = 6 + numShrunkenHotseatIcons = 5 + horizontalMargin = FloatArray(4) { 22f } + borderSpaces = listOf( + PointF(16f, 16f), + PointF(16f, 16f), + PointF(16f, 16f), + PointF(16f, 16f) + ).toTypedArray() + allAppsBorderSpaces = listOf( + PointF(16f, 16f), + PointF(16f, 16f), + PointF(16f, 16f), + PointF(16f, 16f) + ).toTypedArray() + hotseatBorderSpaces = FloatArray(4) { 16f } + hotseatColumnSpan = IntArray(4) { 4 } + iconSize = FloatArray(4) { 56f } + allAppsIconSize = FloatArray(4) { 56f } + iconTextSize = FloatArray(4) { 14f } + allAppsIconTextSize = FloatArray(4) { 14f } + minCellSize = listOf( + PointF(64f, 83f), + PointF(64f, 83f), + PointF(64f, 83f), + PointF(64f, 83f) + ).toTypedArray() + allAppsCellSize = listOf( + PointF(64f, 83f), + PointF(64f, 83f), + PointF(64f, 83f), + PointF(64f, 83f) + ).toTypedArray() + inlineQsb = booleanArrayOf( + false, + false, + false, + false + ) + } +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt b/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt new file mode 100644 index 0000000000..80259a59b4 --- /dev/null +++ b/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3 + +import android.graphics.PointF +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.util.WindowBounds +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.`when` as whenever + +/** + * Test for [DeviceProfile] grid dimensions. + * + * This includes workspace, cell layout, shortcut and widget container, cell sizes, etc. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceProfileGridDimensionsTest : DeviceProfileBaseTest() { + + @Test + fun getCellLayoutWidth_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelWidth() { + val tabletWidth = 2560 + val tabletHeight = 1600 + val availableWidth = 2560 + val availableHeight = 1500 + windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0) + useTwoPanels = true + whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true) + whenever(info.densityDpi).thenReturn(320) + inv = newScalableInvariantDeviceProfile() + + val dp = newDP() + + val expectedWorkspaceWidth = availableWidth + val expectedCellLayoutWidth = + (expectedWorkspaceWidth - (dp.workspacePadding.right + dp.workspacePadding.left)) / + dp.panelCount + assertThat(dp.cellLayoutWidth).isEqualTo(expectedCellLayoutWidth) + } + + @Test + fun getCellLayoutHeight_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelHeight() { + val tabletWidth = 2560 + val tabletHeight = 1600 + val availableWidth = 2560 + val availableHeight = 1500 + windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0) + useTwoPanels = true + whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true) + whenever(info.densityDpi).thenReturn(320) + inv = newScalableInvariantDeviceProfile() + + val dp = newDP() + + val expectedWorkspaceHeight = availableHeight + val expectedCellLayoutHeight = + expectedWorkspaceHeight - (dp.workspacePadding.top + dp.workspacePadding.bottom) + assertThat(dp.cellLayoutHeight).isEqualTo(expectedCellLayoutHeight) + } + + @Test + fun getCellSize_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelWidth() { + val tabletWidth = 2560 + val tabletHeight = 1600 + val availableWidth = 2560 + val availableHeight = 1500 + windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0) + useTwoPanels = true + whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true) + whenever(info.densityDpi).thenReturn(320) + inv = newScalableInvariantDeviceProfile() + + val dp = newDP() + + val expectedWorkspaceWidth = availableWidth + val expectedCellLayoutWidth = + (expectedWorkspaceWidth - (dp.workspacePadding.right + dp.workspacePadding.left)) / + dp.panelCount + val expectedShortcutAndWidgetContainerWidth = + expectedCellLayoutWidth - + (dp.cellLayoutPaddingPx.left + dp.cellLayoutPaddingPx.right) + assertThat(dp.getCellSize().x).isEqualTo( + (expectedShortcutAndWidgetContainerWidth - + ((inv!!.numColumns - 1) * dp.cellLayoutBorderSpacePx.x)) / inv!!.numColumns) + val expectedWorkspaceHeight = availableHeight + val expectedCellLayoutHeight = + expectedWorkspaceHeight - (dp.workspacePadding.top + dp.workspacePadding.bottom) + val expectedShortcutAndWidgetContainerHeight = expectedCellLayoutHeight - + (dp.cellLayoutPaddingPx.top + dp.cellLayoutPaddingPx.bottom) + assertThat(dp.getCellSize().y).isEqualTo( + (expectedShortcutAndWidgetContainerHeight - + ((inv!!.numRows - 1) * dp.cellLayoutBorderSpacePx.y)) / inv!!.numRows) + } + + @Test + fun getPanelCount_twoPanelLandscapeScalable4By4GridTablet_equalsTwoPanels() { + val tabletWidth = 2560 + val tabletHeight = 1600 + val availableWidth = 2560 + val availableHeight = 1500 + windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0) + useTwoPanels = true + whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true) + whenever(info.densityDpi).thenReturn(320) + inv = newScalableInvariantDeviceProfile() + + val dp = newDP() + + assertThat(dp.panelCount).isEqualTo(2) + } +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/HotseatShownIconsTest.kt b/tests/src/com/android/launcher3/HotseatShownIconsTest.kt new file mode 100644 index 0000000000..593239d6f7 --- /dev/null +++ b/tests/src/com/android/launcher3/HotseatShownIconsTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY +import com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE +import com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test for [DeviceProfile] + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class HotseatShownIconsTest : DeviceProfileBaseTest() { + + @Test + fun hotseat_size_is_normal_for_handhelds() { + initializeVarsForPhone() + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_PHONE + } + + val dp = newDP() + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(4) + } + + @Test + fun hotseat_size_is_max_when_large_screen() { + initializeVarsForTablet(isLandscape = true) + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_MULTI_DISPLAY + } + useTwoPanels = true + + val dp = newDP() + + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + + @Test + fun hotseat_size_is_shrunk_if_needed_when_large_screen() { + initializeVarsForTablet(isLandscape = true) + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_MULTI_DISPLAY + inlineQsb = booleanArrayOf( + false, + false, + false, + true // two panels landscape + ) + } + useTwoPanels = true + + isGestureMode = false + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isTrue() + assertThat(dp.numShownHotseatIcons).isEqualTo(5) + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + } + + /** + * For consistency, the hotseat should shrink if any orientation on the device type has an + * inline qsb + */ + @Test + fun hotseat_size_is_shrunk_even_in_portrait_when_large_screen() { + initializeVarsForTablet() + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_MULTI_DISPLAY + inlineQsb = booleanArrayOf( + false, + false, + false, + true // two panels landscape + ) + } + useTwoPanels = true + + isGestureMode = false + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(5) + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + } + + @Test + fun hotseat_size_is_default_when_small_screen() { + initializeVarsForPhone() + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_MULTI_DISPLAY + } + useTwoPanels = true + + val dp = newDP() + + assertThat(dp.numShownHotseatIcons).isEqualTo(4) + } + + @Test + fun hotseat_size_is_not_shrunk_on_gesture_tablet() { + initializeVarsForTablet(isLandscape = true) + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_TABLET + inlineQsb = booleanArrayOf( + false, + true, // landscape + false, + false + ) + numShownHotseatIcons = 6 + } + + isGestureMode = true + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isTrue() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + } + + @Test + fun hotseat_size_is_shrunk_if_needed_on_tablet() { + initializeVarsForTablet(isLandscape = true) + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_TABLET + inlineQsb = booleanArrayOf( + false, + true, // landscape + false, + false + ) + numShownHotseatIcons = 6 + } + + isGestureMode = false + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isTrue() + assertThat(dp.numShownHotseatIcons).isEqualTo(5) + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + } + + /** + * For consistency, the hotseat should shrink if any orientation on the device type has an + * inline qsb + */ + @Test + fun hotseat_size_is_shrunk_even_in_portrait_on_tablet() { + initializeVarsForTablet() + inv = newScalableInvariantDeviceProfile().apply { + deviceType = TYPE_TABLET + inlineQsb = booleanArrayOf( + false, + true, // landscape + false, + false + ) + numShownHotseatIcons = 6 + } + + isGestureMode = false + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(5) + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + assertThat(dp.numShownHotseatIcons).isEqualTo(6) + } + } + +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/InlineQsbTest.kt b/tests/src/com/android/launcher3/InlineQsbTest.kt new file mode 100644 index 0000000000..905c1e1a0f --- /dev/null +++ b/tests/src/com/android/launcher3/InlineQsbTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test for [DeviceProfile] + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class InlineQsbTest : DeviceProfileBaseTest() { + + @Test + fun qsb_is_not_inline_for_phones() { + initializeVarsForPhone() + + val dp = newDP() + + assertThat(dp.isQsbInline).isFalse() + } + + @Test + fun qsb_is_inline_for_tablet_portrait() { + initializeVarsForTablet() + inv = newScalableInvariantDeviceProfile().apply { + inlineQsb = booleanArrayOf( + false, + true, // landscape + false, + false + ) + } + + val dp = DeviceProfile( + context, + inv, + info, + windowBounds, + isMultiWindowMode, + transposeLayoutWithOrientation, + useTwoPanels, + isGestureMode + ) + + assertThat(dp.isQsbInline).isFalse() + } + + @Test + fun qsb_is_inline_for_tablet_landscape() { + initializeVarsForTablet(isLandscape = true) + inv = newScalableInvariantDeviceProfile().apply { + inlineQsb = booleanArrayOf( + false, + true, // landscape + false, + false + ) + numColumns = 6 + numRows = 5 + numShownHotseatIcons = 6 + } + + val dp = newDP() + + if (dp.hotseatQsbHeight > 0) { + assertThat(dp.isQsbInline).isTrue() + } else { // Launcher3 doesn't have QSB height + assertThat(dp.isQsbInline).isFalse() + } + } + + /** + * This test is to make sure that a tablet doesn't inline the QSB if the layout doesn't support + */ + @Test + fun qsb_is_not_inline_for_tablet_landscape_without_inline() { + initializeVarsForTablet(isLandscape = true) + useTwoPanels = true + + val dp = newDP() + + assertThat(dp.isQsbInline).isFalse() + } + +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java index 032a7b4418..92e3e64e4b 100644 --- a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java +++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java @@ -27,12 +27,14 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import java.util.UUID; +import java.util.concurrent.TimeUnit; /** @@ -43,6 +45,8 @@ import java.util.UUID; public class PromiseIconUiTest extends AbstractLauncherUiTest { private int mSessionId = -1; + // TODO(b/202985412): Revert to default timeout when PackageManager bug is fixed. + private static final long PROMISE_ICON_TIMEOUT = TimeUnit.SECONDS.toMillis(60); @Override public void setUp() throws Exception { @@ -73,6 +77,7 @@ public class PromiseIconUiTest extends AbstractLauncherUiTest { } @Test + @ScreenRecord // b/202985412 public void testPromiseIcon_addedFromEligibleSession() throws Throwable { final String appLabel = "Test Promise App " + UUID.randomUUID().toString(); final ItemOperator findPromiseApp = (info, view) -> @@ -83,7 +88,8 @@ public class PromiseIconUiTest extends AbstractLauncherUiTest { // Verify promise icon is added waitForLauncherCondition("Test Promise App not found on workspace", launcher -> - launcher.getWorkspace().getFirstMatch(findPromiseApp) != null); + launcher.getWorkspace().getFirstMatch(findPromiseApp) != null, + PROMISE_ICON_TIMEOUT); // Remove session mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); @@ -91,10 +97,12 @@ public class PromiseIconUiTest extends AbstractLauncherUiTest { // Verify promise icon is removed waitForLauncherCondition("Test Promise App not removed from workspace", launcher -> - launcher.getWorkspace().getFirstMatch(findPromiseApp) == null); + launcher.getWorkspace().getFirstMatch(findPromiseApp) == null, + PROMISE_ICON_TIMEOUT); } @Test + @ScreenRecord // b/202985412 public void testPromiseIcon_notAddedFromIneligibleSession() throws Throwable { final String appLabel = "Test Promise App " + UUID.randomUUID().toString(); final ItemOperator findPromiseApp = (info, view) -> @@ -108,6 +116,7 @@ public class PromiseIconUiTest extends AbstractLauncherUiTest { // Verify promise icon is not added waitForLauncherCondition("Test Promise App not found on workspace", launcher -> - launcher.getWorkspace().getFirstMatch(findPromiseApp) == null); + launcher.getWorkspace().getFirstMatch(findPromiseApp) == null, + PROMISE_ICON_TIMEOUT); } } diff --git a/tests/src/com/android/launcher3/deviceemulator/DisplayEmulator.java b/tests/src/com/android/launcher3/deviceemulator/DisplayEmulator.java new file mode 100644 index 0000000000..31468c5336 --- /dev/null +++ b/tests/src/com/android/launcher3/deviceemulator/DisplayEmulator.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.deviceemulator; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.UserHandle; +import android.view.Display; +import android.view.IWindowManager; +import android.view.WindowManagerGlobal; + +import androidx.test.uiautomator.UiDevice; + +import com.android.launcher3.deviceemulator.models.DeviceEmulationData; +import com.android.launcher3.tapl.LauncherInstrumentation; +import com.android.launcher3.util.window.WindowManagerProxy; + +import java.util.concurrent.Callable; + + +public class DisplayEmulator { + Context mContext; + LauncherInstrumentation mLauncher; + DisplayEmulator(Context context, LauncherInstrumentation launcher) { + mContext = context; + mLauncher = launcher; + } + + /** + * By changing the WindowManagerProxy we can override the window insets information + **/ + private IWindowManager changeWindowManagerInstance(DeviceEmulationData deviceData) { + WindowManagerProxy.INSTANCE.initializeForTesting( + new TestWindowManagerProxy(mContext, deviceData)); + return WindowManagerGlobal.getWindowManagerService(); + } + + public T emulate(DeviceEmulationData device, String grid, Callable runInEmulation) + throws Exception { + WindowManagerProxy original = WindowManagerProxy.INSTANCE.get(mContext); + // Set up emulation + final int userId = UserHandle.myUserId(); + WindowManagerProxy.INSTANCE.initializeForTesting( + new TestWindowManagerProxy(mContext, device)); + IWindowManager wm = changeWindowManagerInstance(device); + // Change density twice to force display controller to reset its state + wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, device.density / 2, userId); + wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, device.density, userId); + wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, device.width, device.height); + wm.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 1); + + // Set up grid + setGrid(grid); + try { + return runInEmulation.call(); + } finally { + // Clear emulation + WindowManagerProxy.INSTANCE.initializeForTesting(original); + UiDevice.getInstance(getInstrumentation()).executeShellCommand("cmd window reset"); + } + } + + private void setGrid(String gridType) { + // When the grid changes, the desktop arrangement get stored in SQL and we need to wait to + // make sure there is no SQL operations running and get SQL_BUSY error, that's why we need + // to call mLauncher.waitForLauncherInitialized(); + mLauncher.waitForLauncherInitialized(); + String testProviderAuthority = mContext.getPackageName() + ".grid_control"; + Uri gridUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(testProviderAuthority) + .appendPath("default_grid") + .build(); + ContentValues values = new ContentValues(); + values.put("name", gridType); + mContext.getContentResolver().update(gridUri, values, null, null); + } +} diff --git a/tests/src/com/android/launcher3/deviceemulator/TestWindowManagerProxy.java b/tests/src/com/android/launcher3/deviceemulator/TestWindowManagerProxy.java new file mode 100644 index 0000000000..cbea688a9a --- /dev/null +++ b/tests/src/com/android/launcher3/deviceemulator/TestWindowManagerProxy.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.deviceemulator; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Display; +import android.view.WindowInsets; + +import com.android.launcher3.deviceemulator.models.DeviceEmulationData; +import com.android.launcher3.util.RotationUtils; +import com.android.launcher3.util.WindowBounds; +import com.android.launcher3.util.window.CachedDisplayInfo; +import com.android.launcher3.util.window.WindowManagerProxy; + +public class TestWindowManagerProxy extends WindowManagerProxy { + + private final DeviceEmulationData mDevice; + + public TestWindowManagerProxy(Context context, DeviceEmulationData device) { + super(true); + mDevice = device; + } + + @Override + public boolean isInternalDisplay(Display display) { + return display.getDisplayId() == Display.DEFAULT_DISPLAY; + } + + @Override + protected int getDimenByName(Resources res, String resName) { + Integer mock = mDevice.resourceOverrides.get(resName); + return mock != null ? mock : super.getDimenByName(res, resName); + } + + @Override + protected int getDimenByName(Resources res, String resName, String fallback) { + return getDimenByName(res, resName); + } + + @Override + public CachedDisplayInfo getDisplayInfo(Context context, Display display) { + int rotation = display.getRotation(); + Point size = new Point(mDevice.width, mDevice.height); + RotationUtils.rotateSize(size, rotation); + Rect cutout = new Rect(mDevice.cutout); + RotationUtils.rotateRect(cutout, rotation); + return new CachedDisplayInfo(getDisplayId(display), size, rotation, cutout); + } + + @Override + public WindowBounds getRealBounds(Context windowContext, Display display, + CachedDisplayInfo info) { + return estimateInternalDisplayBounds(windowContext) + .get(getDisplayId(display)).second[display.getRotation()]; + } + + @Override + public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets, + Rect outInsets) { + outInsets.set(getRealBounds(context, context.getDisplay(), + getDisplayInfo(context, context.getDisplay())).insets); + return oldInsets; + } +} diff --git a/tests/src/com/android/launcher3/deviceemulator/models/DeviceEmulationData.java b/tests/src/com/android/launcher3/deviceemulator/models/DeviceEmulationData.java new file mode 100644 index 0000000000..8d275cc04b --- /dev/null +++ b/tests/src/com/android/launcher3/deviceemulator/models/DeviceEmulationData.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.deviceemulator.models; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT; +import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE; +import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE; +import static com.android.launcher3.ResourceUtils.getDimenByName; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Build; +import android.util.ArrayMap; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.IOUtils; +import com.android.launcher3.util.IntArray; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.Map; + +public class DeviceEmulationData { + + public final int width; + public final int height; + public final int density; + public final String name; + public final String[] grids; + public final Rect cutout; + public final Map resourceOverrides; + + private static final String[] EMULATED_SYSTEM_RESOURCES = new String[]{ + NAVBAR_HEIGHT, + NAVBAR_HEIGHT_LANDSCAPE, + NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE, + "status_bar_height", + }; + + public DeviceEmulationData(int width, int height, int density, Rect cutout, String name, + String[] grid, + Map resourceOverrides) { + this.width = width; + this.height = height; + this.density = density; + this.name = name; + this.grids = grid; + this.cutout = cutout; + this.resourceOverrides = resourceOverrides; + } + + public static DeviceEmulationData deviceFromJSON(JSONObject json) throws JSONException { + int width = json.getInt("width"); + int height = json.getInt("height"); + int density = json.getInt("density"); + String name = json.getString("name"); + + JSONArray gridArray = json.getJSONArray("grids"); + String[] grids = new String[gridArray.length()]; + for (int i = 0, count = grids.length; i < count; i++) { + grids[i] = gridArray.getString(i); + } + + IntArray deviceCutout = IntArray.fromConcatString(json.getString("cutout")); + Rect cutout = new Rect(deviceCutout.get(0), deviceCutout.get(1), deviceCutout.get(2), + deviceCutout.get(3)); + + + JSONObject resourceOverridesJson = json.getJSONObject("resourceOverrides"); + Map resourceOverrides = new ArrayMap<>(); + for (String key : resourceOverridesJson.keySet()) { + resourceOverrides.put(key, resourceOverridesJson.getInt(key)); + } + return new DeviceEmulationData(width, height, density, cutout, name, grids, + resourceOverrides); + } + + @Override + public String toString() { + JSONObject json = new JSONObject(); + try { + json.put("width", width); + json.put("height", height); + json.put("density", density); + json.put("name", name); + json.put("cutout", IntArray.wrap( + cutout.left, cutout.top, cutout.right, cutout.bottom).toConcatString()); + + JSONArray gridArray = new JSONArray(); + Arrays.stream(grids).forEach(gridArray::put); + json.put("grids", gridArray); + + + JSONObject resourceOverrides = new JSONObject(); + for (Map.Entry e : this.resourceOverrides.entrySet()) { + resourceOverrides.put(e.getKey(), e.getValue()); + } + json.put("resourceOverrides", resourceOverrides); + } catch (Exception e) { + e.printStackTrace(); + } + return json.toString(); + } + + public static DeviceEmulationData getCurrentDeviceData(Context context) { + DisplayController.Info info = DisplayController.INSTANCE.get(context).getInfo(); + String[] grids = InvariantDeviceProfile.INSTANCE.get(context) + .parseAllGridOptions(context).stream() + .map(go -> go.name).toArray(String[]::new); + String code = Build.MODEL.replaceAll("\\s", "").toLowerCase(); + + Map resourceOverrides = new ArrayMap<>(); + for (String s : EMULATED_SYSTEM_RESOURCES) { + resourceOverrides.put(s, getDimenByName(s, context.getResources(), 0)); + } + return new DeviceEmulationData(info.currentSize.x, info.currentSize.y, + info.getDensityDpi(), info.cutout, code, grids, resourceOverrides); + } + + public static DeviceEmulationData getDevice(String deviceCode) throws Exception { + return DeviceEmulationData.deviceFromJSON(readJSON().getJSONObject(deviceCode)); + } + + private static JSONObject readJSON() throws Exception { + Context context = getInstrumentation().getContext(); + Resources myRes = context.getResources(); + int resId = myRes.getIdentifier("devices", "raw", context.getPackageName()); + try (InputStream is = myRes.openRawResource(resId)) { + return new JSONObject(new String(IOUtils.toByteArray(is))); + } + } + +} diff --git a/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java index 23e62356c4..9c15309f82 100644 --- a/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java +++ b/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.util.ActivityContextWrapper; import com.android.launcher3.util.Executors; import org.junit.Before; @@ -47,7 +48,7 @@ public final class FolderNameProviderTest { @Before public void setUp() { - mContext = getApplicationContext(); + mContext = new ActivityContextWrapper(getApplicationContext()); mItem1 = new WorkspaceItemInfo(new AppInfo( new ComponentName("a.b.c", "a.b.c/a.b.c.d"), "title1", diff --git a/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt b/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt new file mode 100644 index 0000000000..d26381dc2f --- /dev/null +++ b/tests/src/com/android/launcher3/model/AbstractWorkspaceModelTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherSettings +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.ContentWriter +import com.android.launcher3.util.GridOccupancy +import com.android.launcher3.util.IntArray +import com.android.launcher3.util.IntSparseArrayMap +import com.android.launcher3.util.LauncherModelHelper +import java.util.UUID + +/** + * Base class for workspace related tests. + */ +abstract class AbstractWorkspaceModelTest { + companion object { + val emptyScreenSpaces = listOf(Rect(0, 0, 5, 5)) + val fullScreenSpaces = emptyList() + val nonEmptyScreenSpaces = listOf(Rect(1, 2, 3, 4)) + } + + protected lateinit var mTargetContext: Context + protected lateinit var mIdp: InvariantDeviceProfile + protected lateinit var mAppState: LauncherAppState + protected lateinit var mModelHelper: LauncherModelHelper + protected lateinit var mExistingScreens: IntArray + protected lateinit var mNewScreens: IntArray + protected lateinit var mScreenOccupancy: IntSparseArrayMap + + open fun setup() { + mModelHelper = LauncherModelHelper() + mTargetContext = mModelHelper.sandboxContext + mIdp = InvariantDeviceProfile.INSTANCE[mTargetContext] + mIdp.numRows = 5 + mIdp.numColumns = mIdp.numRows + mAppState = LauncherAppState.getInstance(mTargetContext) + mExistingScreens = IntArray() + mScreenOccupancy = IntSparseArrayMap() + mNewScreens = IntArray() + } + + open fun tearDown() { + mModelHelper.destroy() + } + + + /** + * Sets up workspaces with the given screen IDs with some items and a 2x2 space. + */ + fun setupWorkspaces(screenIdsWithItems: List) { + var nextItemId = 1 + screenIdsWithItems.forEach { screenId -> + nextItemId = setupWorkspace(nextItemId, screenId, nonEmptyScreenSpaces) + } + } + + /** + * Sets up the given workspaces with the given spaces, and fills the remaining space with items. + */ + fun setupWorkspacesWithSpaces( + screen0: List? = null, + screen1: List? = null, + screen2: List? = null, + screen3: List? = null, + ) = listOf(screen0, screen1, screen2, screen3) + .let(this::setupWithSpaces) + + private fun setupWithSpaces(workspaceSpaces: List?>) { + var nextItemId = 1 + workspaceSpaces.forEachIndexed { screenId, spaces -> + if (spaces != null) { + nextItemId = setupWorkspace(nextItemId, screenId, spaces) + } + } + } + + private fun setupWorkspace(startId: Int, screenId: Int, spaces: List): Int { + return mModelHelper.executeSimpleTask { dataModel -> + writeWorkspaceWithSpaces(dataModel, startId, screenId, spaces) + } + } + + private fun writeWorkspaceWithSpaces( + bgDataModel: BgDataModel, + itemStartId: Int, + screenId: Int, + spaces: List, + ): Int { + var itemId = itemStartId + val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows) + occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true) + spaces.forEach { spaceRect -> + occupancy.markCells(spaceRect, false) + } + mExistingScreens.add(screenId) + mScreenOccupancy.append(screenId, occupancy) + for (x in 0 until mIdp.numColumns) { + for (y in 0 until mIdp.numRows) { + if (!occupancy.cells[x][y]) { + continue + } + val info = getExistingItem() + info.id = itemId++ + info.screenId = screenId + info.cellX = x + info.cellY = y + info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP + bgDataModel.addItem(mTargetContext, info, false) + val writer = ContentWriter(mTargetContext) + info.writeToValues(writer) + writer.put(LauncherSettings.Favorites._ID, info.id) + mTargetContext.contentResolver.insert( + LauncherSettings.Favorites.CONTENT_URI, + writer.getValues(mTargetContext) + ) + } + } + return itemId + } + + fun getExistingItem() = WorkspaceItemInfo() + .apply { intent = Intent().setComponent(ComponentName("a", "b")) } + + fun getNewItem(): WorkspaceItemInfo { + val itemPackage = UUID.randomUUID().toString() + return WorkspaceItemInfo() + .apply { intent = Intent().setComponent(ComponentName(itemPackage, itemPackage)) } + } +} + +data class NewItemSpace( + val screenId: Int, + val cellX: Int, + val cellY: Int +) { + fun toIntArray() = intArrayOf(screenId, cellX, cellY) + + companion object { + fun fromIntArray(array: kotlin.IntArray) = NewItemSpace(array[0], array[1], array[2]) + } +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java deleted file mode 100644 index 8a4590a388..0000000000 --- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.android.launcher3.model; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Rect; -import android.util.Pair; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.model.BgDataModel.Callbacks; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.util.ContentWriter; -import com.android.launcher3.util.Executors; -import com.android.launcher3.util.GridOccupancy; -import com.android.launcher3.util.IntArray; -import com.android.launcher3.util.IntSparseArrayMap; -import com.android.launcher3.util.LauncherModelHelper; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; - -import java.util.ArrayList; -import java.util.List; - -/** - * Tests for {@link AddWorkspaceItemsTask} - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AddWorkspaceItemsTaskTest { - - private final ComponentName mComponent1 = new ComponentName("a", "b"); - private final ComponentName mComponent2 = new ComponentName("b", "b"); - - private Context mTargetContext; - private InvariantDeviceProfile mIdp; - private LauncherAppState mAppState; - private LauncherModelHelper mModelHelper; - - private IntArray mExistingScreens; - private IntArray mNewScreens; - private IntSparseArrayMap mScreenOccupancy; - - @Before - public void setup() { - mModelHelper = new LauncherModelHelper(); - mTargetContext = mModelHelper.sandboxContext; - mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext); - mIdp.numColumns = mIdp.numRows = 5; - mAppState = LauncherAppState.getInstance(mTargetContext); - - mExistingScreens = new IntArray(); - mScreenOccupancy = new IntSparseArrayMap<>(); - mNewScreens = new IntArray(); - } - - @After - public void tearDown() { - mModelHelper.destroy(); - } - - private AddWorkspaceItemsTask newTask(ItemInfo... items) { - List> list = new ArrayList<>(); - for (ItemInfo item : items) { - list.add(Pair.create(item, null)); - } - return new AddWorkspaceItemsTask(list); - } - - @Test - public void testFindSpaceForItem_prefers_second() throws Exception { - // First screen has only one hole of size 1 - int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3)); - - // Second screen has 2 holes of sizes 3x2 and 2x3 - setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5)); - - int[] spaceFound = newTask().findSpaceForItem( - mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1); - assertEquals(1, spaceFound[0]); - assertTrue(mScreenOccupancy.get(spaceFound[0]) - .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1)); - - // Find a larger space - spaceFound = newTask().findSpaceForItem( - mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3); - assertEquals(2, spaceFound[0]); - assertTrue(mScreenOccupancy.get(spaceFound[0]) - .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3)); - } - - @Test - public void testFindSpaceForItem_adds_new_screen() throws Exception { - // First screen has 2 holes of sizes 3x2 and 2x3 - setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5)); - - IntArray oldScreens = mExistingScreens.clone(); - int[] spaceFound = newTask().findSpaceForItem( - mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3); - assertFalse(oldScreens.contains(spaceFound[0])); - assertTrue(mNewScreens.contains(spaceFound[0])); - } - - @Test - public void testAddItem_existing_item_ignored() throws Exception { - WorkspaceItemInfo info = new WorkspaceItemInfo(); - info.intent = new Intent().setComponent(mComponent1); - - // Setup a screen with a hole - setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3)); - - // Nothing was added - assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty()); - } - - @Test - public void testAddItem_some_items_added() throws Exception { - Callbacks callbacks = mock(Callbacks.class); - Executors.MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(callbacks)).get(); - - WorkspaceItemInfo info = new WorkspaceItemInfo(); - info.intent = new Intent().setComponent(mComponent1); - - WorkspaceItemInfo info2 = new WorkspaceItemInfo(); - info2.intent = new Intent().setComponent(mComponent2); - - // Setup a screen with a hole - setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3)); - - mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run(); - ArgumentCaptor notAnimated = ArgumentCaptor.forClass(ArrayList.class); - ArgumentCaptor animated = ArgumentCaptor.forClass(ArrayList.class); - - // only info2 should be added because info was already added to the workspace - // in setupWorkspaceWithHoles() - verify(callbacks).bindAppsAdded(any(IntArray.class), notAnimated.capture(), - animated.capture()); - assertTrue(notAnimated.getValue().isEmpty()); - - assertEquals(1, animated.getValue().size()); - assertTrue(animated.getValue().contains(info2)); - } - - private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception { - return mModelHelper.executeSimpleTask( - model -> writeWorkspaceWithHoles(model, startId, screenId, holes)); - } - - private int writeWorkspaceWithHoles( - BgDataModel bgDataModel, int startId, int screenId, Rect... holes) { - GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows); - occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true); - for (Rect r : holes) { - occupancy.markCells(r, false); - } - - mExistingScreens.add(screenId); - mScreenOccupancy.append(screenId, occupancy); - - for (int x = 0; x < mIdp.numColumns; x++) { - for (int y = 0; y < mIdp.numRows; y++) { - if (!occupancy.cells[x][y]) { - continue; - } - - WorkspaceItemInfo info = new WorkspaceItemInfo(); - info.intent = new Intent().setComponent(mComponent1); - info.id = startId++; - info.screenId = screenId; - info.cellX = x; - info.cellY = y; - info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - bgDataModel.addItem(mTargetContext, info, false); - - ContentWriter writer = new ContentWriter(mTargetContext); - info.writeToValues(writer); - writer.put(Favorites._ID, info.id); - mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI, - writer.getValues(mTargetContext)); - } - } - return startId; - } -} diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt new file mode 100644 index 0000000000..65d938b91a --- /dev/null +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.util.Pair +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.Executors +import com.android.launcher3.util.IntArray +import com.android.launcher3.util.same +import com.android.launcher3.util.eq +import com.android.launcher3.util.any +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.times +import org.mockito.Mockito.`when` as whenever + +/** + * Tests for [AddWorkspaceItemsTask] + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class AddWorkspaceItemsTaskTest : AbstractWorkspaceModelTest() { + + @Captor + private lateinit var mAnimatedItemArgumentCaptor: ArgumentCaptor> + + @Captor + private lateinit var mNotAnimatedItemArgumentCaptor: ArgumentCaptor> + + @Mock + private lateinit var mDataModelCallbacks: BgDataModel.Callbacks + + @Mock + private lateinit var mWorkspaceItemSpaceFinder: WorkspaceItemSpaceFinder + + + @Before + override fun setup() { + super.setup() + MockitoAnnotations.initMocks(this) + Executors.MAIN_EXECUTOR.submit { mModelHelper.model.addCallbacks(mDataModelCallbacks) } + .get() + } + + @After + override fun tearDown() { + super.tearDown() + } + + @Test + fun givenNewItemAndNonEmptyPages_whenExecuteTask_thenAddNewItem() { + val itemToAdd = getNewItem() + val nonEmptyScreenIds = listOf(0, 1, 2) + givenNewItemSpaces(NewItemSpace(1, 2, 2)) + + val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd) + + assertThat(addedItems.size).isEqualTo(1) + assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1) + assertThat(addedItems.first().isAnimated).isTrue() + verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1) + } + + @Test + fun givenNewAndExistingItems_whenExecuteTask_thenOnlyAddNewItem() { + val itemsToAdd = arrayOf( + getNewItem(), + getExistingItem() + ) + givenNewItemSpaces(NewItemSpace(1, 0, 0)) + val nonEmptyScreenIds = listOf(0) + + val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd) + + assertThat(addedItems.size).isEqualTo(1) + assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1) + assertThat(addedItems.first().isAnimated).isTrue() + verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1) + } + + @Test + fun givenOnlyExistingItem_whenExecuteTask_thenDoNotAddItem() { + val itemToAdd = getExistingItem() + givenNewItemSpaces(NewItemSpace(1, 0, 0)) + val nonEmptyScreenIds = listOf(0) + + val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd) + + assertThat(addedItems.size).isEqualTo(0) + verifyZeroInteractions(mWorkspaceItemSpaceFinder, mDataModelCallbacks) + } + + @Test + fun givenNonSequentialScreenIds_whenExecuteTask_thenReturnNewScreenId() { + val itemToAdd = getNewItem() + givenNewItemSpaces(NewItemSpace(2, 1, 3)) + val nonEmptyScreenIds = listOf(0, 2, 3) + + val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd) + + assertThat(addedItems.size).isEqualTo(1) + assertThat(addedItems.first().itemInfo.screenId).isEqualTo(2) + assertThat(addedItems.first().isAnimated).isTrue() + verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1) + } + + @Test + fun givenMultipleItems_whenExecuteTask_thenAddThem() { + val itemsToAdd = arrayOf( + getNewItem(), + getExistingItem(), + getNewItem(), + getNewItem(), + getExistingItem(), + ) + givenNewItemSpaces( + NewItemSpace(1, 3, 3), + NewItemSpace(2, 0, 0), + NewItemSpace(2, 0, 1), + ) + val nonEmptyScreenIds = listOf(0, 1) + + val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd) + + // Only the new items should be added + assertThat(addedItems.size).isEqualTo(3) + + // Items that are added to the first screen should not be animated + val itemsAddedToFirstScreen = addedItems.filter { it.itemInfo.screenId == 1 } + assertThat(itemsAddedToFirstScreen.size).isEqualTo(1) + assertThat(itemsAddedToFirstScreen.first().isAnimated).isFalse() + + // Items that are added to the second screen should be animated + val itemsAddedToSecondScreen = addedItems.filter { it.itemInfo.screenId == 2 } + assertThat(itemsAddedToSecondScreen.size).isEqualTo(2) + itemsAddedToSecondScreen.forEach { + assertThat(it.isAnimated).isTrue() + } + verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 3) + } + + /** + * Sets up the item space data that will be returned from WorkspaceItemSpaceFinder. + */ + private fun givenNewItemSpaces(vararg newItemSpaces: NewItemSpace) { + val spaceStack = newItemSpaces.toMutableList() + whenever( + mWorkspaceItemSpaceFinder.findSpaceForItem( + any(), + any(), + any(), + any(), + any(), + any() + ) + ) + .then { spaceStack.removeFirst().toIntArray() } + } + + /** + * Verifies if WorkspaceItemSpaceFinder was called with proper arguments and how many times was + * it called. + */ + private fun verifyItemSpaceFinderCall( + nonEmptyScreenIds: List, + numberOfExpectedCall: Int + ) { + verify(mWorkspaceItemSpaceFinder, times(numberOfExpectedCall)) + .findSpaceForItem( + same(mAppState), same(mModelHelper.bgDataModel), + eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())), eq(IntArray()), eq(1), eq(1) + ) + } + + /** + * Sets up the workspaces with items, executes the task, collects the added items from the + * model callback then returns it. + */ + private fun testAddItems( + nonEmptyScreenIds: List, + vararg itemsToAdd: WorkspaceItemInfo + ): List { + setupWorkspaces(nonEmptyScreenIds) + val task = newTask(*itemsToAdd) + var updateCount = 0 + mModelHelper.executeTaskForTest(task) + .forEach { + updateCount++ + it.run() + } + + val addedItems = mutableListOf() + if (updateCount > 0) { + verify(mDataModelCallbacks).bindAppsAdded( + any(), + mNotAnimatedItemArgumentCaptor.capture(), mAnimatedItemArgumentCaptor.capture() + ) + addedItems.addAll(mAnimatedItemArgumentCaptor.value.map { AddedItem(it, true) }) + addedItems.addAll(mNotAnimatedItemArgumentCaptor.value.map { AddedItem(it, false) }) + + } + + return addedItems + } + + /** + * Creates the task with the given items and replaces the WorkspaceItemSpaceFinder dependency + * with a mock. + */ + private fun newTask(vararg items: ItemInfo): AddWorkspaceItemsTask = + items.map { Pair.create(it, Any()) } + .toMutableList() + .let { AddWorkspaceItemsTask(it, mWorkspaceItemSpaceFinder) } +} + +private data class AddedItem( + val itemInfo: ItemInfo, + val isAnimated: Boolean +) diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java deleted file mode 100644 index 005389e5bb..0000000000 --- a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.model; - -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; -import static com.android.launcher3.LauncherSettings.Favorites.TMP_CONTENT_URI; -import static com.android.launcher3.provider.LauncherDbUtils.dropTable; -import static com.android.launcher3.util.LauncherModelHelper.APP_ICON; -import static com.android.launcher3.util.LauncherModelHelper.DESKTOP; -import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT; -import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT; -import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.graphics.Point; -import android.os.Process; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.pm.UserCache; -import com.android.launcher3.util.LauncherModelHelper; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.HashMap; -import java.util.HashSet; - -/** Unit tests for {@link GridSizeMigrationTaskV2} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class GridSizeMigrationTaskV2Test { - - private LauncherModelHelper mModelHelper; - private Context mContext; - private SQLiteDatabase mDb; - - private HashSet mValidPackages; - private InvariantDeviceProfile mIdp; - - private final String testPackage1 = "com.android.launcher3.validpackage1"; - private final String testPackage2 = "com.android.launcher3.validpackage2"; - private final String testPackage3 = "com.android.launcher3.validpackage3"; - private final String testPackage4 = "com.android.launcher3.validpackage4"; - private final String testPackage5 = "com.android.launcher3.validpackage5"; - private final String testPackage6 = "com.android.launcher3.validpackage6"; - private final String testPackage7 = "com.android.launcher3.validpackage7"; - private final String testPackage8 = "com.android.launcher3.validpackage8"; - private final String testPackage9 = "com.android.launcher3.validpackage9"; - private final String testPackage10 = "com.android.launcher3.validpackage10"; - - @Before - public void setUp() { - mModelHelper = new LauncherModelHelper(); - mContext = mModelHelper.sandboxContext; - mDb = mModelHelper.provider.getDb(); - - mValidPackages = new HashSet<>(); - mValidPackages.add(TEST_PACKAGE); - mValidPackages.add(testPackage1); - mValidPackages.add(testPackage2); - mValidPackages.add(testPackage3); - mValidPackages.add(testPackage4); - mValidPackages.add(testPackage5); - mValidPackages.add(testPackage6); - mValidPackages.add(testPackage7); - mValidPackages.add(testPackage8); - mValidPackages.add(testPackage9); - mValidPackages.add(testPackage10); - - mIdp = InvariantDeviceProfile.INSTANCE.get(mContext); - - long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser( - Process.myUserHandle()); - dropTable(mDb, LauncherSettings.Favorites.TMP_TABLE); - LauncherSettings.Favorites.addTableToDb(mDb, userSerial, false, - LauncherSettings.Favorites.TMP_TABLE); - } - - @After - public void tearDown() { - mModelHelper.destroy(); - } - - @Test - public void testMigration() throws Exception { - int[] srcHotseatItems = { - mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI), - mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI), - -1, - mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI), - mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI), - }; - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage5, 5, TMP_CONTENT_URI); - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage6, 6, TMP_CONTENT_URI); - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage8, 8, TMP_CONTENT_URI); - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage9, 9, TMP_CONTENT_URI); - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 3, testPackage10, 10, TMP_CONTENT_URI); - - int[] destHotseatItems = { - -1, - mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2), - -1, - }; - mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7); - - mIdp.numDatabaseHotseatIcons = 4; - mIdp.numColumns = 4; - mIdp.numRows = 4; - GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages); - GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages); - GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader, - destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows)); - task.migrate(mIdp); - - // Check hotseat items - Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); - assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons); - int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); - int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 0); - assertTrue(c.getString(intentIndex).contains(testPackage1)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 1); - assertTrue(c.getString(intentIndex).contains(testPackage2)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 2); - assertTrue(c.getString(intentIndex).contains(testPackage3)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 3); - assertTrue(c.getString(intentIndex).contains(testPackage4)); - c.close(); - - // Check workspace items - c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, - LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_DESKTOP, null, null, null); - intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); - int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX); - int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY); - - HashMap locMap = new HashMap<>(); - while (c.moveToNext()) { - locMap.put( - Intent.parseUri(c.getString(intentIndex), 0).getPackage(), - new Point(c.getInt(cellXIndex), c.getInt(cellYIndex))); - } - c.close(); - - assertEquals(locMap.size(), 6); - assertEquals(new Point(0, 2), locMap.get(testPackage8)); - assertEquals(new Point(0, 3), locMap.get(testPackage6)); - assertEquals(new Point(1, 3), locMap.get(testPackage10)); - assertEquals(new Point(2, 3), locMap.get(testPackage5)); - assertEquals(new Point(3, 3), locMap.get(testPackage9)); - } - - @Test - public void migrateToLargerHotseat() { - int[] srcHotseatItems = { - mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI), - mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI), - mModelHelper.addItem(APP_ICON, 2, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI), - mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI), - }; - - int numSrcDatabaseHotseatIcons = srcHotseatItems.length; - mIdp.numDatabaseHotseatIcons = 6; - mIdp.numColumns = 4; - mIdp.numRows = 4; - GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages); - GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages); - GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader, - destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows)); - task.migrate(mIdp); - - // Check hotseat items - Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); - assertEquals(c.getCount(), numSrcDatabaseHotseatIcons); - int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); - int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 0); - assertTrue(c.getString(intentIndex).contains(testPackage1)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 1); - assertTrue(c.getString(intentIndex).contains(testPackage2)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 2); - assertTrue(c.getString(intentIndex).contains(testPackage3)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 3); - assertTrue(c.getString(intentIndex).contains(testPackage4)); - - c.close(); - } - - @Test - public void migrateFromLargerHotseat() { - int[] srcHotseatItems = { - mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI), - -1, - mModelHelper.addItem(SHORTCUT, 2, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI), - mModelHelper.addItem(APP_ICON, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI), - mModelHelper.addItem(SHORTCUT, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI), - mModelHelper.addItem(APP_ICON, 5, HOTSEAT, 0, 0, testPackage5, 5, TMP_CONTENT_URI), - }; - - mIdp.numDatabaseHotseatIcons = 4; - mIdp.numColumns = 4; - mIdp.numRows = 4; - GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages); - GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb, - LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages); - GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader, - destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows)); - task.migrate(mIdp); - - // Check hotseat items - Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT}, - "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null); - assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons); - int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN); - int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 0); - assertTrue(c.getString(intentIndex).contains(testPackage1)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 1); - assertTrue(c.getString(intentIndex).contains(testPackage2)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 2); - assertTrue(c.getString(intentIndex).contains(testPackage3)); - c.moveToNext(); - assertEquals(c.getInt(screenIndex), 3); - assertTrue(c.getString(intentIndex).contains(testPackage4)); - - c.close(); - } -} diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.kt new file mode 100644 index 0000000000..90d7b434df --- /dev/null +++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.kt @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model + +import android.content.Context +import android.content.Intent +import android.database.sqlite.SQLiteDatabase +import android.graphics.Point +import android.os.Process +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherFiles +import com.android.launcher3.LauncherSettings.Favorites.* +import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.model.GridSizeMigrationTaskV2.DbReader +import com.android.launcher3.pm.UserCache +import com.android.launcher3.provider.LauncherDbUtils +import com.android.launcher3.util.LauncherModelHelper +import com.android.launcher3.util.LauncherModelHelper.* +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [GridSizeMigrationTaskV2] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class GridSizeMigrationTaskV2Test { + private lateinit var modelHelper: LauncherModelHelper + private lateinit var context: Context + private lateinit var db: SQLiteDatabase + private lateinit var validPackages: Set + private lateinit var idp: InvariantDeviceProfile + private val testPackage1 = "com.android.launcher3.validpackage1" + private val testPackage2 = "com.android.launcher3.validpackage2" + private val testPackage3 = "com.android.launcher3.validpackage3" + private val testPackage4 = "com.android.launcher3.validpackage4" + private val testPackage5 = "com.android.launcher3.validpackage5" + private val testPackage6 = "com.android.launcher3.validpackage6" + private val testPackage7 = "com.android.launcher3.validpackage7" + private val testPackage8 = "com.android.launcher3.validpackage8" + private val testPackage9 = "com.android.launcher3.validpackage9" + private val testPackage10 = "com.android.launcher3.validpackage10" + + @Before + fun setUp() { + modelHelper = LauncherModelHelper() + context = modelHelper.sandboxContext + db = modelHelper.provider.db + + validPackages = setOf( + TEST_PACKAGE, + testPackage1, + testPackage2, + testPackage3, + testPackage4, + testPackage5, + testPackage6, + testPackage7, + testPackage8, + testPackage9, + testPackage10 + ) + + idp = InvariantDeviceProfile.INSTANCE[context] + val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle()) + LauncherDbUtils.dropTable(db, TMP_TABLE) + addTableToDb(db, userSerial, false, TMP_TABLE) + } + + @After + fun tearDown() { + modelHelper.destroy() + } + + /** + * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is + * not needed anymore + */ + @Test + @Throws(Exception::class) + fun testMigration() { + // Src Hotseat icons + modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI) + modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI) + modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI) + // Src grid icons + // _ _ _ _ _ + // _ _ _ _ 5 + // _ _ 6 _ 7 + // _ _ 8 _ 9 + // _ _ _ _ _ + modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage5, 5, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage6, 6, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage7, 7, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage8, 8, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 3, testPackage9, 9, TMP_CONTENT_URI) + + // Dest hotseat icons + modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2) + // Dest grid icons + modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage10) + + idp.numDatabaseHotseatIcons = 4 + idp.numColumns = 4 + idp.numRows = 4 + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Check hotseat items + var c = context.contentResolver.query( + CONTENT_URI, + arrayOf(SCREEN, INTENT), + "container=$CONTAINER_HOTSEAT", + null, + SCREEN, + null + ) ?: throw IllegalStateException() + + assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons) + + val screenIndex = c.getColumnIndex(SCREEN) + var intentIndex = c.getColumnIndex(INTENT) + c.moveToNext() + assertThat(c.getInt(screenIndex).toLong()).isEqualTo(0) + assertThat(c.getString(intentIndex)).contains(testPackage1) + c.moveToNext() + assertThat(c.getInt(screenIndex).toLong()).isEqualTo(1) + assertThat(c.getString(intentIndex)).contains(testPackage2) + c.moveToNext() + assertThat(c.getInt(screenIndex).toLong()).isEqualTo(2) + assertThat(c.getString(intentIndex)).contains(testPackage3) + c.moveToNext() + assertThat(c.getInt(screenIndex).toLong()).isEqualTo(3) + assertThat(c.getString(intentIndex)).contains(testPackage4) + c.close() + + // Check workspace items + c = context.contentResolver.query( + CONTENT_URI, + arrayOf(CELLX, CELLY, INTENT), + "container=$CONTAINER_DESKTOP", + null, + null, + null + ) ?: throw IllegalStateException() + + intentIndex = c.getColumnIndex(INTENT) + val cellXIndex = c.getColumnIndex(CELLX) + val cellYIndex = c.getColumnIndex(CELLY) + val locMap = HashMap() + while (c.moveToNext()) { + locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] = + Point(c.getInt(cellXIndex), c.getInt(cellYIndex)) + } + c.close() + // Expected dest grid icons + // _ _ _ _ + // 5 6 7 8 + // 9 _ 10_ + // _ _ _ _ + assertThat(locMap.size.toLong()).isEqualTo(6) + assertThat(locMap[testPackage5]).isEqualTo(Point(0, 1)) + assertThat(locMap[testPackage6]).isEqualTo(Point(1, 1)) + assertThat(locMap[testPackage7]).isEqualTo(Point(2, 1)) + assertThat(locMap[testPackage8]).isEqualTo(Point(3, 1)) + assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2)) + assertThat(locMap[testPackage10]).isEqualTo(Point(2, 2)) + } + + @Test + fun migrateToLargerHotseat() { + val srcHotseatItems = intArrayOf( + modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI), + modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI), + modelHelper.addItem(APP_ICON, 2, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI), + modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI) + ) + val numSrcDatabaseHotseatIcons = srcHotseatItems.size + idp.numDatabaseHotseatIcons = 6 + idp.numColumns = 4 + idp.numRows = 4 + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Check hotseat items + val c = context.contentResolver.query( + CONTENT_URI, + arrayOf(SCREEN, INTENT), + "container=$CONTAINER_HOTSEAT", + null, + SCREEN, + null + ) ?: throw IllegalStateException() + + assertThat(c.count.toLong()).isEqualTo(numSrcDatabaseHotseatIcons.toLong()) + val screenIndex = c.getColumnIndex(SCREEN) + val intentIndex = c.getColumnIndex(INTENT) + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(0) + assertThat(c.getString(intentIndex)).contains(testPackage1) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(1) + assertThat(c.getString(intentIndex)).contains(testPackage2) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(2) + assertThat(c.getString(intentIndex)).contains(testPackage3) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(3) + assertThat(c.getString(intentIndex)).contains(testPackage4) + + c.close() + } + + @Test + fun migrateFromLargerHotseat() { + modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI) + modelHelper.addItem(SHORTCUT, 2, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI) + modelHelper.addItem(SHORTCUT, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 5, HOTSEAT, 0, 0, testPackage5, 5, TMP_CONTENT_URI) + + idp.numDatabaseHotseatIcons = 4 + idp.numColumns = 4 + idp.numRows = 4 + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Check hotseat items + val c = context.contentResolver.query( + CONTENT_URI, + arrayOf(SCREEN, INTENT), + "container=$CONTAINER_HOTSEAT", + null, + SCREEN, + null + ) ?: throw IllegalStateException() + + assertThat(c.count.toLong()).isEqualTo(idp.numDatabaseHotseatIcons.toLong()) + val screenIndex = c.getColumnIndex(SCREEN) + val intentIndex = c.getColumnIndex(INTENT) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(0) + assertThat(c.getString(intentIndex)).contains(testPackage1) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(1) + assertThat(c.getString(intentIndex)).contains(testPackage2) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(2) + assertThat(c.getString(intentIndex)).contains(testPackage3) + + c.moveToNext() + assertThat(c.getInt(screenIndex)).isEqualTo(3) + assertThat(c.getString(intentIndex)).contains(testPackage4) + + c.close() + } + + /** + * Migrating from a smaller grid to a large one should keep the pages + * if the column difference is less than 2 + */ + @Test + @Throws(Exception::class) + fun migrateFromSmallerGridSmallDifference() { + enableNewMigrationLogic("4,4") + + // Setup src grid + modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage1, 5, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage2, 6, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 3, 1, testPackage3, 7, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 3, 2, testPackage4, 8, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 2, DESKTOP, 3, 3, testPackage5, 9, TMP_CONTENT_URI) + + idp.numDatabaseHotseatIcons = 4 + idp.numColumns = 6 + idp.numRows = 5 + + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Get workspace items + val c = context.contentResolver.query( + CONTENT_URI, + arrayOf(INTENT, SCREEN), + "container=$CONTAINER_DESKTOP", + null, + null, + null + ) ?: throw IllegalStateException() + val intentIndex = c.getColumnIndex(INTENT) + val screenIndex = c.getColumnIndex(SCREEN) + + // Get in which screen the icon is + val locMap = HashMap() + while (c.moveToNext()) { + locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] = + c.getInt(screenIndex) + } + c.close() + assertThat(locMap.size).isEqualTo(5) + assertThat(locMap[testPackage1]).isEqualTo(0) + assertThat(locMap[testPackage2]).isEqualTo(0) + assertThat(locMap[testPackage3]).isEqualTo(1) + assertThat(locMap[testPackage4]).isEqualTo(1) + assertThat(locMap[testPackage5]).isEqualTo(2) + + disableNewMigrationLogic() + } + + /** + * Migrating from a smaller grid to a large one should reflow the pages + * if the column difference is more than 2 + */ + @Test + @Throws(Exception::class) + fun migrateFromSmallerGridBigDifference() { + enableNewMigrationLogic("2,2") + + // Setup src grid + modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 1, testPackage1, 5, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 1, testPackage2, 6, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 0, 0, testPackage3, 7, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 1, 0, testPackage4, 8, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 2, DESKTOP, 0, 0, testPackage5, 9, TMP_CONTENT_URI) + + idp.numDatabaseHotseatIcons = 4 + idp.numColumns = 5 + idp.numRows = 5 + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Get workspace items + val c = context.contentResolver.query( + CONTENT_URI, + arrayOf(INTENT, SCREEN), + "container=$CONTAINER_DESKTOP", + null, + null, + null + ) ?: throw IllegalStateException() + + val intentIndex = c.getColumnIndex(INTENT) + val screenIndex = c.getColumnIndex(SCREEN) + + // Get in which screen the icon is + val locMap = HashMap() + while (c.moveToNext()) { + locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] = + c.getInt(screenIndex) + } + c.close() + + // All icons fit the first screen + assertThat(locMap.size).isEqualTo(5) + assertThat(locMap[testPackage1]).isEqualTo(0) + assertThat(locMap[testPackage2]).isEqualTo(0) + assertThat(locMap[testPackage3]).isEqualTo(0) + assertThat(locMap[testPackage4]).isEqualTo(0) + assertThat(locMap[testPackage5]).isEqualTo(0) + disableNewMigrationLogic() + } + + /** + * Migrating from a larger grid to a smaller, we reflow from page 0 + */ + @Test + @Throws(Exception::class) + fun migrateFromLargerGrid() { + enableNewMigrationLogic("5,5") + + // Setup src grid + modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 1, testPackage1, 5, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 1, testPackage2, 6, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 0, 0, testPackage3, 7, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 1, DESKTOP, 1, 0, testPackage4, 8, TMP_CONTENT_URI) + modelHelper.addItem(APP_ICON, 2, DESKTOP, 0, 0, testPackage5, 9, TMP_CONTENT_URI) + + idp.numDatabaseHotseatIcons = 4 + idp.numColumns = 4 + idp.numRows = 4 + val srcReader = DbReader(db, TMP_TABLE, context, validPackages) + val destReader = DbReader(db, TABLE_NAME, context, validPackages) + val task = GridSizeMigrationTaskV2( + context, + db, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows) + ) + task.migrate(DeviceGridState(context), DeviceGridState(idp)) + + // Get workspace items + val c = context.contentResolver.query( + CONTENT_URI, + arrayOf(INTENT, SCREEN), + "container=$CONTAINER_DESKTOP", + null, + null, + null + ) ?: throw IllegalStateException() + val intentIndex = c.getColumnIndex(INTENT) + val screenIndex = c.getColumnIndex(SCREEN) + + // Get in which screen the icon is + val locMap = HashMap() + while (c.moveToNext()) { + locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] = + c.getInt(screenIndex) + } + c.close() + + // All icons fit the first screen + assertThat(locMap.size).isEqualTo(5) + assertThat(locMap[testPackage1]).isEqualTo(0) + assertThat(locMap[testPackage2]).isEqualTo(0) + assertThat(locMap[testPackage3]).isEqualTo(0) + assertThat(locMap[testPackage4]).isEqualTo(0) + assertThat(locMap[testPackage5]).isEqualTo(0) + + disableNewMigrationLogic() + } + + private fun enableNewMigrationLogic(srcGridSize: String) { + context.getSharedPreferences(FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.key, true) + .commit() + context.getSharedPreferences(LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + .edit() + .putString(DeviceGridState.KEY_WORKSPACE_SIZE, srcGridSize) + .commit() + FeatureFlags.initialize(context) + } + + private fun disableNewMigrationLogic() { + context.getSharedPreferences(FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.key, false) + .commit() + } +} \ No newline at end of file diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt new file mode 100644 index 0000000000..bfb1ac64c4 --- /dev/null +++ b/tests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WorkspaceItemSpaceFinder] + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class WorkspaceItemSpaceFinderTest : AbstractWorkspaceModelTest() { + + private val mItemSpaceFinder = WorkspaceItemSpaceFinder() + + @Before + override fun setup() { + super.setup() + } + + @After + override fun tearDown() { + super.tearDown() + } + + private fun findSpace(spanX: Int, spanY: Int): NewItemSpace = + mItemSpaceFinder.findSpaceForItem( + mAppState, mModelHelper.bgDataModel, + mExistingScreens, mNewScreens, spanX, spanY + ) + .let { NewItemSpace.fromIntArray(it) } + + private fun assertRegionVacant(newItemSpace: NewItemSpace, spanX: Int, spanY: Int) { + assertThat( + mScreenOccupancy[newItemSpace.screenId] + .isRegionVacant(newItemSpace.cellX, newItemSpace.cellY, spanX, spanY) + ).isTrue() + } + + @Test + fun justEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnFirstScreenId() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 space + // 2 spaces of sizes 3x2 and 2x3 + screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + ) + + val spaceFound = findSpace(1, 1) + + assertThat(spaceFound.screenId).isEqualTo(1) + assertRegionVacant(spaceFound, 1, 1) + } + + @Test + fun notEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnSecondScreenId() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 space + // 2 spaces of sizes 3x2 and 2x3 + screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + ) + + // Find a larger space + val spaceFound = findSpace(2, 3) + + assertThat(spaceFound.screenId).isEqualTo(2) + assertRegionVacant(spaceFound, 2, 3) + } + + @Test + fun notEnoughSpaceOnExistingScreens_returnNewScreenId() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + // 2 spaces of sizes 3x2 and 2x3 + screen1 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + // 2 spaces of sizes 1x2 and 2x2 + screen2 = listOf(Rect(1, 0, 2, 2), Rect(3, 2, 5, 4)), + ) + + val oldScreens = mExistingScreens.clone() + val spaceFound = findSpace(3, 3) + + assertThat(oldScreens.contains(spaceFound.screenId)).isFalse() + assertThat(mNewScreens.contains(spaceFound.screenId)).isTrue() + } + + @Test + fun firstScreenIsEmptyButSecondIsNotEmpty_returnSecondScreenId() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + // empty screens are skipped + screen2 = listOf(Rect(2, 0, 5, 2)), // 3x2 space + ) + + val spaceFound = findSpace(2, 1) + + assertThat(spaceFound.screenId).isEqualTo(2) + assertRegionVacant(spaceFound, 2, 1) + } + + @Test + fun twoEmptyMiddleScreens_returnThirdScreen() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + // empty screens are skipped + screen3 = listOf(Rect(1, 1, 4, 4)), // 3x3 space + ) + + val spaceFound = findSpace(2, 3) + + assertThat(spaceFound.screenId).isEqualTo(3) + assertRegionVacant(spaceFound, 2, 3) + } + + @Test + fun allExistingPagesAreFull_returnNewScreenId() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + screen1 = fullScreenSpaces, + screen2 = fullScreenSpaces, + ) + + val spaceFound = findSpace(2, 3) + + assertThat(spaceFound.screenId).isEqualTo(3) + assertThat(mNewScreens.contains(spaceFound.screenId)).isTrue() + } + + @Test + fun firstTwoPagesAreFull_and_ThirdPageIsEmpty_returnThirdPage() { + setupWorkspacesWithSpaces( + // 3x2 space on screen 0, but it should be skipped + screen0 = listOf(Rect(2, 0, 5, 2)), + screen1 = fullScreenSpaces, // full screens are skipped + screen2 = fullScreenSpaces, // full screens are skipped + screen3 = emptyScreenSpaces + ) + + val spaceFound = findSpace(3, 1) + + assertThat(spaceFound.screenId).isEqualTo(3) + assertRegionVacant(spaceFound, 3, 1) + } +} diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 075505eb93..6f8b9d2967 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -33,6 +33,7 @@ import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.graphics.Point; import android.os.Debug; import android.os.Process; import android.os.RemoteException; @@ -54,6 +55,8 @@ import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.tapl.HomeAllApps; +import com.android.launcher3.tapl.HomeAppIcon; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType; import com.android.launcher3.tapl.TestHelpers; @@ -65,6 +68,7 @@ import com.android.launcher3.util.Wait; import com.android.launcher3.util.WidgetUtils; import com.android.launcher3.util.rule.FailureWatcher; import com.android.launcher3.util.rule.LauncherActivityRule; +import com.android.launcher3.util.rule.SamplerRule; import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.launcher3.util.rule.ShellCommandRule; import com.android.launcher3.util.rule.TestStabilityRule; @@ -126,14 +130,14 @@ public abstract class AbstractLauncherUiTest { private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher) { sActivityLeakReported = true; return "Activity leak detector has found leaked activities, " - + dumpHprofData(launcher) + "."; + + dumpHprofData(launcher, false) + "."; } - public static String dumpHprofData(LauncherInstrumentation launcher) { + public static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak) { + if (intentionalLeak) return "intentional leak; not generating dump"; + String result; if (sDumpWasGenerated) { - Log.d("b/195319692", "dump has already been generated by another test", - new Exception()); result = "dump has already been generated by another test"; } else { try { @@ -148,7 +152,6 @@ public abstract class AbstractLauncherUiTest { "am dumpheap " + device.getLauncherPackageName() + " " + fileName); } sDumpWasGenerated = true; - Log.d("b/195319692", "sDumpWasGenerated := true", new Exception()); result = "saved memory dump as an artifact"; } catch (Throwable e) { Log.e(TAG, "dumpHprofData failed", e); @@ -225,7 +228,8 @@ public abstract class AbstractLauncherUiTest { @Rule public TestRule mOrderSensitiveRules = RuleChain - .outerRule(new TestStabilityRule()) + .outerRule(new SamplerRule()) + .around(new TestStabilityRule()) .around(mActivityMonitor) .around(getRulesInsideActivityMonitor()); @@ -510,7 +514,7 @@ public abstract class AbstractLauncherUiTest { "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT); } - protected boolean isInBackground(Launcher launcher) { + protected boolean isInLaunchedApp(Launcher launcher) { return launcher == null || !launcher.hasBeenResumed(); } @@ -550,7 +554,7 @@ public abstract class AbstractLauncherUiTest { ordinal == TestProtocol.NORMAL_STATE_ORDINAL); break; } - case ALL_APPS: { + case HOME_ALL_APPS: { assertTrue( "Launcher is not resumed in state: " + expectedContainerType, isResumed); @@ -565,7 +569,8 @@ public abstract class AbstractLauncherUiTest { ordinal == TestProtocol.OVERVIEW_STATE_ORDINAL); break; } - case BACKGROUND: { + case TASKBAR_ALL_APPS: + case LAUNCHED_APP: { assertTrue("Launcher is resumed in state: " + expectedContainerType, !isResumed); assertTrue(TestProtocol.stateOrdinalToString(ordinal), @@ -578,10 +583,11 @@ public abstract class AbstractLauncherUiTest { } } else { assertTrue( - "Container type is not BACKGROUND or FALLBACK_OVERVIEW: " - + expectedContainerType, - expectedContainerType == ContainerType.BACKGROUND || - expectedContainerType == ContainerType.FALLBACK_OVERVIEW); + "Container type is not LAUNCHED_APP, TASKBAR_ALL_APPS " + + "or FALLBACK_OVERVIEW: " + expectedContainerType, + expectedContainerType == ContainerType.LAUNCHED_APP + || expectedContainerType == ContainerType.TASKBAR_ALL_APPS + || expectedContainerType == ContainerType.FALLBACK_OVERVIEW); } } @@ -601,4 +607,30 @@ public abstract class AbstractLauncherUiTest { protected void onLauncherActivityClose(Launcher launcher) { } + + protected HomeAppIcon createShortcutInCenterIfNotExist(String name) { + Point dimension = mLauncher.getWorkspace().getIconGridDimensions(); + return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2); + } + + protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) { + return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y); + } + + protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) { + HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name); + if (homeAppIcon == null) { + HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + allApps.getAppIcon(name).dragToWorkspace(cellX, cellY); + } finally { + allApps.unfreeze(); + } + homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name); + } + return homeAppIcon; + } + + } diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 5b940a88d8..8a97c6ba5b 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -18,12 +18,17 @@ package com.android.launcher3.ui; import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import android.content.Intent; +import android.graphics.Point; + import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -36,20 +41,33 @@ import com.android.launcher3.tapl.AppIconMenu; import com.android.launcher3.tapl.AppIconMenuItem; import com.android.launcher3.tapl.Folder; import com.android.launcher3.tapl.FolderIcon; +import com.android.launcher3.tapl.HomeAllApps; +import com.android.launcher3.tapl.HomeAppIcon; +import com.android.launcher3.tapl.HomeAppIconMenu; +import com.android.launcher3.tapl.HomeAppIconMenuItem; import com.android.launcher3.tapl.Widgets; import com.android.launcher3.tapl.Workspace; +import com.android.launcher3.util.TestUtil; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import com.android.launcher3.widget.picker.WidgetsFullSheet; import com.android.launcher3.widget.picker.WidgetsRecyclerView; import org.junit.Before; -import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.util.Map; + @LargeTest @RunWith(AndroidJUnit4.class) public class TaplTestsLauncher3 extends AbstractLauncherUiTest { private static final String APP_NAME = "LauncherTestApp"; + private static final String DUMMY_APP_NAME = "Aardwolf"; + private static final String MAPS_APP_NAME = "Maps"; + private static final String STORE_APP_NAME = "Play Store"; + private static final String GMAIL_APP_NAME = "Gmail"; @Before public void setUp() throws Exception { @@ -100,7 +118,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { launcher -> assertNotNull("Launcher internal state didn't switch to Showing Menu", launcher.getOptionsPopup())); // Check that pressHome works when the menu is shown. - mLauncher.pressHome(); + mLauncher.goHome(); } @Test @@ -112,7 +130,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } finally { allApps.unfreeze(); } - mLauncher.pressHome(); + mLauncher.goHome(); } public static void runAllAppsTest(AbstractLauncherUiTest test, AllApps allApps) { @@ -174,6 +192,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } @Test + @ScreenRecord // b/202433017 public void testWorkspace() throws Exception { final Workspace workspace = mLauncher.getWorkspace(); @@ -209,7 +228,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL)); // Test starting a workspace app. - final AppIcon app = workspace.getWorkspaceAppIcon("Chrome"); + final HomeAppIcon app = workspace.getWorkspaceAppIcon("Chrome"); assertNotNull("No Chrome app in workspace", app); } @@ -222,7 +241,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { "Launcher activity is the top activity; expecting another activity to be the " + "top " + "one", - test.isInBackground(launcher))); + test.isInLaunchedApp(launcher))); } finally { allApps.unfreeze(); } @@ -231,7 +250,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { @Test @PortraitLandscape public void testAppIconLaunchFromAllAppsFromHome() throws Exception { - final AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); assertTrue("Launcher internal state is not All Apps", isInState(() -> LauncherState.ALL_APPS)); @@ -263,7 +282,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets", getWidgetsScroll(launcher) < flingForwardY)); - mLauncher.pressHome(); + mLauncher.goHome(); waitForLauncherCondition("Widgets were not closed", launcher -> getWidgetsView(launcher) == null); } @@ -280,9 +299,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { @Test @PortraitLandscape public void testLaunchMenuItem() throws Exception { - final AllApps allApps = mLauncher. - getWorkspace(). - switchToAllApps(); + final AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); allApps.freeze(); try { final AppIconMenu menu = allApps. @@ -307,8 +324,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { // 1. Open all apps and wait for load complete. // 2. Drag icon to homescreen. // 3. Verify that the icon works on homescreen. - final AllApps allApps = mLauncher.getWorkspace(). - switchToAllApps(); + final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); allApps.freeze(); try { allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); @@ -319,7 +335,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { executeOnLauncher(launcher -> assertTrue( "Launcher activity is the top activity; expecting another activity to be the top " + "one", - isInBackground(launcher))); + isInLaunchedApp(launcher))); } @Test @@ -328,18 +344,18 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { // 1. Open all apps and wait for load complete. // 2. Find the app and long press it to show shortcuts. // 3. Press icon center until shortcuts appear - final AllApps allApps = mLauncher + final HomeAllApps allApps = mLauncher .getWorkspace() .switchToAllApps(); allApps.freeze(); try { - final AppIconMenu menu = allApps + final HomeAppIconMenu menu = allApps .getAppIcon(APP_NAME) .openDeepShortcutMenu(); - final AppIconMenuItem menuItem0 = menu.getMenuItem(0); - final AppIconMenuItem menuItem2 = menu.getMenuItem(2); + final HomeAppIconMenuItem menuItem0 = menu.getMenuItem(0); + final HomeAppIconMenuItem menuItem2 = menu.getMenuItem(2); - final AppIconMenuItem menuItem; + final HomeAppIconMenuItem menuItem; final String expectedShortcutName = "Shortcut 3"; if (menuItem0.getText().equals(expectedShortcutName)) { @@ -358,50 +374,35 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } } - private AppIcon createShortcutIfNotExist(String name) { - AppIcon appIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name); - if (appIcon == null) { - AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); - allApps.freeze(); - try { - appIcon = allApps.getAppIcon(name); - appIcon.dragToWorkspace(false, false); - } finally { - allApps.unfreeze(); - } - appIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name); - } - return appIcon; - } - - @Ignore("b/205014516") @Test @PortraitLandscape - public void testDragToFolder() throws Exception { - final AppIcon playStoreIcon = createShortcutIfNotExist("Play Store"); - final AppIcon gmailIcon = createShortcutIfNotExist("Gmail"); + public void testDragToFolder() { + // TODO: add the use case to drag an icon to an existing folder. Currently it either fails + // on tablets or phones due to difference in resolution. + final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1); + final HomeAppIcon gmailIcon = createShortcutInCenterIfNotExist(GMAIL_APP_NAME); FolderIcon folderIcon = gmailIcon.dragToIcon(playStoreIcon); - Folder folder = folderIcon.open(); - folder.getAppIcon("Play Store"); - folder.getAppIcon("Gmail"); + folder.getAppIcon(STORE_APP_NAME); + folder.getAppIcon(GMAIL_APP_NAME); Workspace workspace = folder.close(); - assertNull("Gmail should be moved to a folder.", - workspace.tryGetWorkspaceAppIcon("Gmail")); - assertNull("Play Store should be moved to a folder.", - workspace.tryGetWorkspaceAppIcon("Play Store")); + assertNull(STORE_APP_NAME + " should be moved to a folder.", + workspace.tryGetWorkspaceAppIcon(STORE_APP_NAME)); + assertNull(GMAIL_APP_NAME + " should be moved to a folder.", + workspace.tryGetWorkspaceAppIcon(GMAIL_APP_NAME)); - final AppIcon youTubeIcon = createShortcutIfNotExist("YouTube"); - - folderIcon = youTubeIcon.dragToIcon(folderIcon); + final HomeAppIcon mapIcon = createShortcutInCenterIfNotExist(MAPS_APP_NAME); + folderIcon = mapIcon.dragToIcon(folderIcon); folder = folderIcon.open(); - folder.getAppIcon("YouTube"); - folder.close(); + folder.getAppIcon(MAPS_APP_NAME); + workspace = folder.close(); + + assertNull(MAPS_APP_NAME + " should be moved to a folder.", + workspace.tryGetWorkspaceAppIcon(MAPS_APP_NAME)); } - @Ignore("b/205027405") @Test @PortraitLandscape public void testPressBack() throws Exception { @@ -410,14 +411,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { mLauncher.getWorkspace(); waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); - AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); - allApps.freeze(); - try { - allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); - } finally { - allApps.unfreeze(); - } - mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName()); + startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); mLauncher.pressBack(); mLauncher.getWorkspace(); waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); @@ -427,14 +421,122 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { @PortraitLandscape public void testDeleteFromWorkspace() throws Exception { // test delete both built-in apps and user-installed app from workspace - for (String appName : new String[] {"Gmail", "Play Store", APP_NAME}) { - final AppIcon appIcon = createShortcutIfNotExist(appName); - Workspace workspace = mLauncher.getWorkspace().deleteAppIcon(appIcon); + for (String appName : new String[]{"Gmail", "Play Store", APP_NAME}) { + final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName); + Workspace workspace = mLauncher.getWorkspace().deleteAppIcon(homeAppIcon); assertNull(appName + " app was found after being deleted from workspace", workspace.tryGetWorkspaceAppIcon(appName)); } } + private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) { + final HomeAllApps allApps = workspace.switchToAllApps(); + allApps.freeze(); + try { + assertNull(appName + " app was found on all apps after being uninstalled", + allApps.tryGetAppIcon(appName)); + } finally { + allApps.unfreeze(); + } + } + + @Test + @PortraitLandscape + public void testUninstallFromWorkspace() throws Exception { + TestUtil.installDummyApp(); + try { + verifyAppUninstalledFromAllApps( + createShortcutInCenterIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME); + } finally { + TestUtil.uninstallDummyApp(); + } + } + + @Test + @PortraitLandscape + public void testUninstallFromAllApps() throws Exception { + TestUtil.installDummyApp(); + try { + Workspace workspace = mLauncher.getWorkspace(); + final HomeAllApps allApps = workspace.switchToAllApps(); + allApps.freeze(); + try { + workspace = allApps.getAppIcon(DUMMY_APP_NAME).uninstall(); + } finally { + allApps.unfreeze(); + } + verifyAppUninstalledFromAllApps(workspace, DUMMY_APP_NAME); + } finally { + TestUtil.uninstallDummyApp(); + } + } + + @Test + @PortraitLandscape + public void testDragAppIconToWorkspaceCell() throws Exception { + Point[] targets = getCornersAndCenterPositions(); + + for (Point target : targets) { + final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + allApps.getAppIcon(APP_NAME).dragToWorkspace(target.x, target.y); + } finally { + allApps.unfreeze(); + } + // Reset the workspace for the next shortcut creation. + initialize(this); + } + + // test to move a shortcut to other cell. + final HomeAppIcon launcherTestAppIcon = createShortcutInCenterIfNotExist(APP_NAME); + for (Point target : targets) { + launcherTestAppIcon.dragToWorkspace(target.x, target.y); + } + } + + @Test + public void getIconsPosition_afterIconRemoved_notContained() throws IOException { + Point[] gridPositions = getCornersAndCenterPositions(); + createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]); + createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]); + TestUtil.installDummyApp(); + try { + createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]); + Map initialPositions = + mLauncher.getWorkspace().getWorkspaceIconsPositions(); + assertThat(initialPositions.keySet()) + .containsAtLeast(DUMMY_APP_NAME, MAPS_APP_NAME, STORE_APP_NAME); + + mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall(); + + assertNull( + DUMMY_APP_NAME + " app was found after being uninstalled", + mLauncher.getWorkspace().tryGetWorkspaceAppIcon(DUMMY_APP_NAME)); + + Map finalPositions = + mLauncher.getWorkspace().getWorkspaceIconsPositions(); + assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME); + } finally { + TestUtil.uninstallDummyApp(); + } + } + + /** + * @return List of workspace grid coordinates. Those are not pixels. See {@link + * Workspace#getIconGridDimensions()} + */ + private Point[] getCornersAndCenterPositions() { + final Point dimensions = mLauncher.getWorkspace().getIconGridDimensions(); + return new Point[] { + new Point(0, 1), + new Point(0, dimensions.y - 2), + new Point(dimensions.x - 1, 1), + new Point(dimensions.x - 1, dimensions.y - 2), + new Point(dimensions.x / 2, dimensions.y / 2) + }; + } + public static String getAppPackageName() { return getInstrumentation().getContext().getPackageName(); } diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java index 939cfe1ae6..35b4ca6431 100644 --- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java +++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java @@ -25,11 +25,14 @@ import static org.junit.Assert.assertTrue; import android.util.Log; import android.view.View; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsPagedView; import com.android.launcher3.allapps.WorkAdapterProvider; import com.android.launcher3.allapps.WorkEduCard; +import com.android.launcher3.allapps.WorkPausedCard; import com.android.launcher3.allapps.WorkProfileManager; import com.android.launcher3.tapl.LauncherInstrumentation; @@ -38,10 +41,11 @@ import org.junit.Before; import org.junit.Test; import java.util.Objects; +import java.util.function.Predicate; public class WorkProfileTest extends AbstractLauncherUiTest { - private static final int WORK_PAGE = AllAppsContainerView.AdapterHolder.WORK; + private static final int WORK_PAGE = ActivityAllAppsContainerView.AdapterHolder.WORK; private int mProfileUserId; @@ -130,12 +134,14 @@ public class WorkProfileTest extends AbstractLauncherUiTest { return manager.getCurrentState() == WorkProfileManager.STATE_DISABLED; }, LauncherInstrumentation.WAIT_TIME_MS); + waitForWorkCard("Work paused card not shown", view -> view instanceof WorkPausedCard); + // start work profile toggle ON test executeOnLauncher(l -> { - AllAppsContainerView allApps = l.getAppsView(); + ActivityAllAppsContainerView allApps = l.getAppsView(); assertEquals("Work tab is not focused", allApps.getCurrentPage(), WORK_PAGE); - View workPausedCard = allApps.getActiveRecyclerView().findViewHolderForAdapterPosition( - 0).itemView; + View workPausedCard = allApps.getActiveRecyclerView() + .findViewHolderForAdapterPosition(0).itemView; workPausedCard.findViewById(R.id.enable_work_apps).performClick(); }); waitForLauncherCondition("Work profile toggle ON failed", launcher -> { @@ -154,9 +160,19 @@ public class WorkProfileTest extends AbstractLauncherUiTest { l.getAppsView().getWorkManager().reset(); }); - waitForLauncherCondition("Work profile education not shown", - l -> l.getAppsView().getActiveRecyclerView() - .findViewHolderForAdapterPosition(0).itemView instanceof WorkEduCard, - LauncherInstrumentation.WAIT_TIME_MS); + waitForWorkCard("Work profile education not shown", view -> view instanceof WorkEduCard); + } + + private void waitForWorkCard(String message, Predicate workCardCheck) { + waitForLauncherCondition(message, l -> { + l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST); + ViewHolder holder = l.getAppsView().getActiveRecyclerView() + .findViewHolderForAdapterPosition(0); + try { + return holder != null && workCardCheck.test(holder.itemView); + } finally { + l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST); + } + }, LauncherInstrumentation.WAIT_TIME_MS); } } diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java index ccbb662055..0db719e349 100644 --- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java +++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java @@ -15,6 +15,9 @@ */ package com.android.launcher3.ui.widget; +import static android.app.PendingIntent.FLAG_MUTABLE; +import static android.app.PendingIntent.FLAG_ONE_SHOT; + import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName; import static org.junit.Assert.assertNotNull; @@ -42,6 +45,7 @@ import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator; import com.android.launcher3.util.Wait; import com.android.launcher3.util.Wait.Condition; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.Before; @@ -77,6 +81,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ } @Test + @ScreenRecord // b/215673732 public void testPinWidgetNoConfig() throws Throwable { runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId && @@ -85,6 +90,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { } @Test + @ScreenRecord // b/215673732 public void testPinWidgetNoConfig_customPreview() throws Throwable { // Command to set custom preview Intent command = RequestPinItemActivity.getCommandIntent( @@ -98,6 +104,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { } @Test + @ScreenRecord // b/215673732 public void testPinWidgetWithConfig() throws Throwable { runTest("pinWidgetWithConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo && @@ -140,7 +147,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { // Set callback PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0, - new Intent(mCallbackAction), PendingIntent.FLAG_ONE_SHOT); + new Intent(mCallbackAction), FLAG_ONE_SHOT | FLAG_MUTABLE); mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent( RequestPinItemActivity.class, "setCallback").putExtra( RequestPinItemActivity.EXTRA_PARAM + "0", callback)); @@ -165,7 +172,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { } // Go back to home - mLauncher.pressHome(); + mLauncher.goHome(); Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT, mLauncher); } diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java new file mode 100644 index 0000000000..e66810cc3d --- /dev/null +++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.ui.workspace; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; +import android.view.View; +import android.view.ViewGroup; + +import androidx.test.filters.LargeTest; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.icons.ThemedIconDrawable; +import com.android.launcher3.tapl.HomeAllApps; +import com.android.launcher3.tapl.HomeAppIcon; +import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TaplTestsLauncher3; + +import org.junit.Test; + +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * Tests for theme icon support in Launcher + * + * Note running these tests will clear the workspace on the device. + */ +@LargeTest +public class ThemeIconsTest extends AbstractLauncherUiTest { + + private static final String APP_NAME = "ThemeIconTestActivity"; + + @Test + public void testIconWithoutTheme() throws Exception { + setThemeEnabled(false); + TaplTestsLauncher3.initialize(this); + + HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + + try { + HomeAppIcon icon = allApps.getAppIcon(APP_NAME); + executeOnLauncher(l -> verifyIconTheme(l.getAppsView(), false)); + icon.dragToWorkspace(false, false); + executeOnLauncher(l -> verifyIconTheme(l.getWorkspace(), false)); + } finally { + allApps.unfreeze(); + } + } + + @Test + public void testIconWithTheme() throws Exception { + setThemeEnabled(true); + TaplTestsLauncher3.initialize(this); + + HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + + try { + HomeAppIcon icon = allApps.getAppIcon(APP_NAME); + executeOnLauncher(l -> verifyIconTheme(l.getAppsView(), false)); + icon.dragToWorkspace(false, false); + executeOnLauncher(l -> verifyIconTheme(l.getWorkspace(), true)); + } finally { + allApps.unfreeze(); + } + } + + private void verifyIconTheme(ViewGroup parent, boolean isThemed) { + // Find the app icon + Queue viewQueue = new ArrayDeque<>(); + viewQueue.add(parent); + BubbleTextView icon = null; + while (!viewQueue.isEmpty()) { + View view = viewQueue.poll(); + if (view instanceof ViewGroup) { + parent = (ViewGroup) view; + for (int i = parent.getChildCount() - 1; i >= 0; i--) { + viewQueue.add(parent.getChildAt(i)); + } + } else if (view instanceof BubbleTextView) { + BubbleTextView btv = (BubbleTextView) view; + if (APP_NAME.equals(btv.getText())) { + icon = btv; + break; + } + } + } + + assertNotNull(icon.getIcon()); + assertEquals(isThemed, icon.getIcon() instanceof ThemedIconDrawable); + } + + private void setThemeEnabled(boolean isEnabled) throws Exception { + Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(mTargetPackage + ".grid_control") + .appendPath("set_icon_themed") + .build(); + ContentValues values = new ContentValues(); + values.put("boolean_value", isEnabled); + try (ContentProviderClient client = mTargetContext.getContentResolver() + .acquireContentProviderClient(uri)) { + int result = client.update(uri, values, null); + assertTrue(result > 0); + } + } +} diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java index b048cd4d43..f646b504a9 100644 --- a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java +++ b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java @@ -19,6 +19,7 @@ package com.android.launcher3.ui.workspace; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -47,20 +48,12 @@ import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { - Workspace mWorkspace; - @Before public void setUp() throws Exception { super.setUp(); TaplTestsLauncher3.initialize(this); - mWorkspace = mLauncher.getWorkspace(); - } - @Test - public void testDragIconToRightPanel() { - if (!mLauncher.isTwoPanels()) { - return; - } + assumeTrue(mLauncher.isTwoPanels()); // Pre verifying the screens executeOnLauncher(launcher -> { @@ -68,8 +61,14 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { assertItemsOnPage(launcher, 0, "Play Store", "Maps"); assertPageEmpty(launcher, 1); }); + } - mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 1); + @Test + @PortraitLandscape + public void testDragIconToRightPanel() { + Workspace workspace = mLauncher.getWorkspace(); + + workspace.dragIcon(workspace.getHotseatAppIcon("Chrome"), 1); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1); @@ -79,19 +78,65 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { } @Test - public void testDragIconToPage2() { - if (!mLauncher.isTwoPanels()) { - return; - } + @PortraitLandscape + public void testSinglePageDragIconWhenMultiplePageScrollingIsPossible() { + Workspace workspace = mLauncher.getWorkspace(); + + workspace.dragIcon(workspace.getHotseatAppIcon("Chrome"), 2); + + workspace.flingBackward(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 3); - // Pre verifying the screens executeOnLauncher(launcher -> { - assertPagesExist(launcher, 0, 1); - assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Chrome"); + assertItemsOnPage(launcher, 3, "Maps"); }); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 3); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3, 4, 5); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Chrome"); + assertPageEmpty(launcher, 3); + assertPageEmpty(launcher, 4); + assertItemsOnPage(launcher, 5, "Maps"); + }); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), -1); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Chrome"); + assertItemsOnPage(launcher, 3, "Maps"); + }); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), -1); + + workspace.flingForward(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Chrome"), -2); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Chrome", "Play Store"); + assertItemsOnPage(launcher, 1, "Maps"); + }); + } + + @Test + @PortraitLandscape + public void testDragIconToPage2() { + Workspace workspace = mLauncher.getWorkspace(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 2); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); @@ -103,19 +148,11 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { } @Test + @PortraitLandscape public void testDragIconToPage3() { - if (!mLauncher.isTwoPanels()) { - return; - } + Workspace workspace = mLauncher.getWorkspace(); - // Pre verifying the screens - executeOnLauncher(launcher -> { - assertPagesExist(launcher, 0, 1); - assertItemsOnPage(launcher, 0, "Play Store", "Maps"); - assertPageEmpty(launcher, 1); - }); - - mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Phone"), 3); + workspace.dragIcon(workspace.getHotseatAppIcon("Phone"), 3); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); @@ -126,22 +163,59 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { }); } - @Test - public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() { - if (!mLauncher.isTwoPanels()) { - return; - } + @PortraitLandscape + public void testMultiplePageDragIcon() { + Workspace workspace = mLauncher.getWorkspace(); + + workspace.dragIcon(workspace.getHotseatAppIcon("Messages"), 2); + + workspace.flingBackward(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 5); - // Pre verifying the screens executeOnLauncher(launcher -> { - assertPagesExist(launcher, 0, 1); - assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPagesExist(launcher, 0, 1, 2, 3, 4, 5); + assertItemsOnPage(launcher, 0, "Play Store"); assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Messages"); + assertPageEmpty(launcher, 3); + assertPageEmpty(launcher, 4); + assertItemsOnPage(launcher, 5, "Maps"); }); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 3); - mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 0); + workspace.flingBackward(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Messages"), 4); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 4, 5, 6, 7); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertPageEmpty(launcher, 4); + assertItemsOnPage(launcher, 5, "Maps"); + assertItemsOnPage(launcher, 6, "Messages"); + assertPageEmpty(launcher, 7); + }); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Messages"), -3); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 4, 5); + assertItemsOnPage(launcher, 0, "Play Store"); + assertItemsOnPage(launcher, 1, "Messages"); + assertPageEmpty(launcher, 4); + assertItemsOnPage(launcher, 5, "Maps"); + }); + } + + @Test + @PortraitLandscape + public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() { + Workspace workspace = mLauncher.getWorkspace(); + + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 3); + workspace.dragIcon(workspace.getHotseatAppIcon("Chrome"), 0); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); @@ -151,7 +225,7 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { assertItemsOnPage(launcher, 3, "Maps"); }); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), -1); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), -1); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); @@ -163,8 +237,8 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { // Move Chrome to the right panel as well, to make sure pages are not deleted whichever // page is the empty one - mWorkspace.flingForward(); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Chrome"), 1); + workspace.flingForward(); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Chrome"), 1); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); @@ -175,57 +249,40 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { }); } - @Test + @PortraitLandscape public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() { - if (!mLauncher.isTwoPanels()) { - return; - } + Workspace workspace = mLauncher.getWorkspace(); - // Pre verifying the screens - executeOnLauncher(launcher -> { - assertPagesExist(launcher, 0, 1); - assertItemsOnPage(launcher, 0, "Play Store", "Maps"); - assertPageEmpty(launcher, 1); - }); - - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), 2); - mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Camera"), 1); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Play Store"), 2); + workspace.dragIcon(workspace.getHotseatAppIcon("Chrome"), 1); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3); assertItemsOnPage(launcher, 0, "Maps"); assertPageEmpty(launcher, 1); assertItemsOnPage(launcher, 2, "Play Store"); - assertItemsOnPage(launcher, 3, "Camera"); + assertItemsOnPage(launcher, 3, "Chrome"); }); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Camera"), -1); - mWorkspace.flingForward(); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), -2); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Chrome"), -1); + workspace.flingForward(); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Play Store"), -2); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1); assertItemsOnPage(launcher, 0, "Play Store", "Maps"); - assertItemsOnPage(launcher, 1, "Camera"); + assertItemsOnPage(launcher, 1, "Chrome"); }); } @Test + @PortraitLandscape public void testMiddleEmptyPagesGetRemoved() { - if (!mLauncher.isTwoPanels()) { - return; - } + Workspace workspace = mLauncher.getWorkspace(); - // Pre verifying the screens - executeOnLauncher(launcher -> { - assertPagesExist(launcher, 0, 1); - assertItemsOnPage(launcher, 0, "Play Store", "Maps"); - assertPageEmpty(launcher, 1); - }); - - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); - mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Messages"), 3); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 2); + workspace.dragIcon(workspace.getHotseatAppIcon("Messages"), 3); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 2, 3, 4, 5); @@ -237,8 +294,8 @@ public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { assertItemsOnPage(launcher, 5, "Messages"); }); - mWorkspace.flingBackward(); - mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); + workspace.flingBackward(); + workspace.dragIcon(workspace.getWorkspaceAppIcon("Maps"), 2); executeOnLauncher(launcher -> { assertPagesExist(launcher, 0, 1, 4, 5); diff --git a/tests/src/com/android/launcher3/util/KotlinMockitoHelpers.kt b/tests/src/com/android/launcher3/util/KotlinMockitoHelpers.kt new file mode 100644 index 0000000000..57db13ac45 --- /dev/null +++ b/tests/src/com/android/launcher3/util/KotlinMockitoHelpers.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util + +/** + * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects + * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not + * be null"). To fix this, we can use methods that modify the return type to be nullable. This + * causes Kotlin to skip the null checks. + */ + +import org.mockito.ArgumentCaptor +import org.mockito.Mockito + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun eq(obj: T): T = Mockito.eq(obj) + +/** + * Returns Mockito.same() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun same(obj: T): T = Mockito.same(obj) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun any(type: Class): T = Mockito.any(type) +inline fun any(): T = any(T::class.java) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun nullable(): T? = Mockito.nullable(T::class.java) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun argumentCaptor(): ArgumentCaptor = + ArgumentCaptor.forClass(T::class.java) + +/** + * Helper function for creating new mocks, without the need to pass in a [Class] instance. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun mock(): T = Mockito.mock(T::class.java) + +/** + * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when + * kotlin tests are mocking kotlin objects and the methods take non-null parameters: + * + * java.lang.NullPointerException: capture() must not be null + */ +class KotlinArgumentCaptor constructor(clazz: Class) { + private val wrapped: ArgumentCaptor = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun kotlinArgumentCaptor(): KotlinArgumentCaptor = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun withArgCaptor(block: KotlinArgumentCaptor.() -> Unit): T = + kotlinArgumentCaptor().apply { block() }.value diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java index 59966eee04..33249592c2 100644 --- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -67,6 +67,7 @@ import com.android.launcher3.pm.UserCache; import com.android.launcher3.testing.TestInformationProvider; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.widget.custom.CustomWidgetManager; import org.mockito.ArgumentCaptor; @@ -501,7 +502,7 @@ public class LauncherModelHelper { LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, - ItemInstallQueue.INSTANCE); + ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE); mPm = spy(getBaseContext().getPackageManager()); mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); } diff --git a/tests/src/com/android/launcher3/util/MultiAdditivePropertyTest.kt b/tests/src/com/android/launcher3/util/MultiAdditivePropertyTest.kt new file mode 100644 index 0000000000..309d055bad --- /dev/null +++ b/tests/src/com/android/launcher3/util/MultiAdditivePropertyTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util + +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [MultiAdditivePropertyFactory] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiAdditivePropertyTest { + + private val received = mutableListOf() + + private val factory = + object : MultiAdditivePropertyFactory("Test", View.TRANSLATION_X) { + override fun apply(obj: View?, value: Float) { + received.add(value) + } + } + + private val p1 = factory.get(1) + private val p2 = factory.get(2) + private val p3 = factory.get(3) + + @Test + fun set_sameIndexes_allApplied() { + val v1 = 50f + val v2 = 100f + p1.set(null, v1) + p1.set(null, v1) + p1.set(null, v2) + + assertThat(received).containsExactly(v1, v1, v2) + } + + @Test + fun set_differentIndexes_aggregationApplied() { + val v1 = 50f + val v2 = 100f + val v3 = 150f + p1.set(null, v1) + p2.set(null, v2) + p3.set(null, v3) + + assertThat(received).containsExactly(v1, v1 + v2, v1 + v2 + v3) + } +} diff --git a/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt b/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt new file mode 100644 index 0000000000..7d92214d9b --- /dev/null +++ b/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util + +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [MultiScalePropertyFactory] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiScalePropertyTest { + + private val received = mutableListOf() + + private val factory = + object : MultiScalePropertyFactory("Test") { + override fun apply(obj: View?, value: Float) { + received.add(value) + } + } + + private val p1 = factory.get(1) + private val p2 = factory.get(2) + private val p3 = factory.get(3) + + @Test + fun set_multipleSame_bothAppliedd() { + p1.set(null, 0.5f) + p1.set(null, 0.5f) + + assertThat(received).containsExactly(0.5f, 0.5f) + } + + @Test + fun set_differentIndexes_oneValuesNotCounted() { + val v1 = 0.5f + val v2 = 1.0f + p1.set(null, v1) + p2.set(null, v2) + + assertThat(received).containsExactly(v1, v1) + } + + @Test + fun set_onlyOneSetToOne_oneApplied() { + p1.set(null, 1.0f) + + assertThat(received).containsExactly(1.0f) + } + + @Test + fun set_onlyOneLessThanOne_applied() { + p1.set(null, 0.5f) + + assertThat(received).containsExactly(0.5f) + } + + @Test + fun set_differentIndexes_boundToMin() { + val v1 = 0.5f + val v2 = 0.6f + p1.set(null, v1) + p2.set(null, v2) + + assertThat(received).containsExactly(v1, v1) + } + + @Test + fun set_allHigherThanOne_boundToMax() { + val v1 = 3.0f + val v2 = 2.0f + val v3 = 1.0f + p1.set(null, v1) + p2.set(null, v2) + p3.set(null, v3) + + assertThat(received).containsExactly(v1, v1, v1) + } + + @Test + fun set_differentIndexes_firstModified_aggregationApplied() { + val v1 = 0.5f + val v2 = 0.6f + val v3 = 4f + p1.set(null, v1) + p2.set(null, v2) + p3.set(null, v3) + + assertThat(received).containsExactly(v1, v1, v1 * v2 * v3) + } +} diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java index 65aaa24527..4c41d7ec4f 100644 --- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java +++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java @@ -84,7 +84,7 @@ public class FailureWatcher extends TestWatcher { @Override protected void failed(Throwable e, Description description) { - onError(mDevice, description, e); + onError(mLauncher, description, e); } static File diagFile(Description description, String prefix, String ext) { @@ -93,7 +93,9 @@ public class FailureWatcher extends TestWatcher { + description.getMethodName() + "." + ext); } - public static void onError(UiDevice device, Description description, Throwable e) { + public static void onError(LauncherInstrumentation launcher, Description description, + Throwable e) { + final UiDevice device = launcher.getDevice(); Log.d("b/196820244", "onError 1"); if (device == null) return; Log.d("b/196820244", "onError 2"); @@ -128,6 +130,13 @@ public class FailureWatcher extends TestWatcher { } dumpCommand("logcat -d -s TestRunner", diagFile(description, "FilteredLogcat", "txt")); + + // Dump bugreport + final String systemAnomalyMessage = launcher.getSystemAnomalyMessage(false, false); + if (systemAnomalyMessage != null) { + Log.d(TAG, "Saving bugreport, system anomaly message: " + systemAnomalyMessage, e); + dumpCommand("bugreportz -s", diagFile(description, "Bugreport", "zip")); + } } private static void dumpStringCommand(String cmd, OutputStream out) throws IOException { diff --git a/tests/src/com/android/launcher3/util/rule/SamplerRule.java b/tests/src/com/android/launcher3/util/rule/SamplerRule.java new file mode 100644 index 0000000000..6125f2a8d2 --- /dev/null +++ b/tests/src/com/android/launcher3/util/rule/SamplerRule.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util.rule; + +import android.os.SystemClock; + +import androidx.test.InstrumentationRegistry; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A rule that generates a file that helps diagnosing cases when the test process was terminated + * because the test execution took too long, and tests that ran for too long even without being + * terminated. If the process was terminated or the test was long, the test leaves an artifact with + * stack traces of all threads, every SAMPLE_INTERVAL_MS. This will help understanding where we + * stuck. + */ +public class SamplerRule implements TestRule { + private static final int TOO_LONG_TEST_MS = 180000; + private static final int SAMPLE_INTERVAL_MS = 3000; + + public static Thread startThread(Description description) { + Thread thread = + new Thread() { + @Override + public void run() { + // Write all-threads stack stace every SAMPLE_INTERVAL_MS while the test + // is running. + // After the test finishes, delete that file. If the test process is + // terminated due to timeout, the trace file won't be deleted. + final File file = getFile(); + + final long startTime = SystemClock.elapsedRealtime(); + try (OutputStreamWriter outputStreamWriter = + new OutputStreamWriter( + new BufferedOutputStream( + new FileOutputStream(file)))) { + writeSamples(outputStreamWriter); + } catch (IOException | InterruptedException e) { + // Simply suppressing the exceptions, nothing to do here. + } finally { + // If the process is not killed, then there was no test timeout, and + // we are not interested in the trace file, unless the test ran too + // long. + if (SystemClock.elapsedRealtime() - startTime < TOO_LONG_TEST_MS) { + file.delete(); + } + } + } + + private File getFile() { + final String strDate = new SimpleDateFormat("HH:mm:ss").format(new Date()); + + final String descStr = description.getTestClass().getSimpleName() + "." + + description.getMethodName(); + return artifactFile( + "ThreadStackSamples-" + strDate + "-" + descStr + ".txt"); + } + + private void writeSamples(OutputStreamWriter writer) + throws IOException, InterruptedException { + int count = 0; + while (true) { + writer.write( + "#" + + (count++) + + " =============================================\r\n"); + for (StackTraceElement[] stack : getAllStackTraces().values()) { + writer.write("---------------------\r\n"); + for (StackTraceElement frame : stack) { + writer.write(frame.toString() + "\r\n"); + } + } + writer.flush(); + + sleep(SAMPLE_INTERVAL_MS); + } + } + }; + + thread.start(); + return thread; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final Thread traceThread = startThread(description); + try { + base.evaluate(); + } finally { + traceThread.interrupt(); + traceThread.join(); + } + } + }; + } + + private static File artifactFile(String fileName) { + return new File( + InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir(), + fileName); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java index 0582bc9557..98eb32e818 100644 --- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java +++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java @@ -22,8 +22,6 @@ import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiObject2; -import com.android.launcher3.testing.TestProtocol; - import java.util.regex.Pattern; public class AddToHomeScreenPrompt { @@ -44,19 +42,10 @@ public class AddToHomeScreenPrompt { public void addAutomatically() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - if (mLauncher.getNavigationModel() - != LauncherInstrumentation.NavigationModel.THREE_BUTTON) { - if (!mLauncher.isLauncher3()) { - mLauncher.expectEvent( - TestProtocol.SEQUENCE_TIS, - LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS); - mLauncher.expectEvent( - TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS); - } - } - mLauncher.waitForObjectInContainer( - mWidgetCell.getParent().getParent().getParent().getParent(), - By.text(ADD_AUTOMATICALLY)).click(); + mLauncher.clickObject( + mLauncher.waitForObjectInContainer( + mWidgetCell.getParent().getParent().getParent().getParent(), + By.text(ADD_AUTOMATICALLY))); mLauncher.waitUntilLauncherObjectGone(getSelector()); } } diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java index 78301e48cd..bfb115d1b8 100644 --- a/tests/tapl/com/android/launcher3/tapl/AllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java @@ -22,6 +22,7 @@ import android.os.Bundle; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.Direction; @@ -35,7 +36,7 @@ import java.util.stream.Collectors; /** * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview. */ -public class AllApps extends LauncherInstrumentation.VisibleContainer { +public abstract class AllApps extends LauncherInstrumentation.VisibleContainer { private static final int MAX_SCROLL_ATTEMPTS = 40; private final int mHeight; @@ -50,14 +51,8 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { // Wait for the recycler to populate. mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class)); verifyNotFrozen("All apps freeze flags upon opening all apps"); - mIconHeight = mLauncher.getTestInfo( - TestProtocol.REQUEST_ICON_HEIGHT). - getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); - } - - @Override - protected LauncherInstrumentation.ContainerType getContainerType() { - return LauncherInstrumentation.ContainerType.ALL_APPS; + mIconHeight = mLauncher.getTestInfo(TestProtocol.REQUEST_ICON_HEIGHT) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); } private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler, @@ -79,7 +74,7 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { LauncherInstrumentation.log("hasClickableIcon: icon has insufficient height"); return false; } - if (iconCenterInSearchBox(allAppsContainer, icon)) { + if (hasSearchBox() && iconCenterInSearchBox(allAppsContainer, icon)) { LauncherInstrumentation.log("hasClickableIcon: icon center is under search box"); return false; } @@ -98,21 +93,21 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { } /** - * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make - * sure the icon is visible. + * Finds an icon. If the icon doesn't exist, return null. + * Scrolls the app list when needed to make sure the icon is visible. * * @param appName name of the app. - * @return The app. + * @return The app if found, and null if not found. */ - @NonNull - public AppIcon getAppIcon(String appName) { + @Nullable + public AppIcon tryGetAppIcon(String appName) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "getting app icon " + appName + " on all apps")) { final UiObject2 allAppsContainer = verifyActiveContainer(); final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view"); - final UiObject2 searchBox = getSearchBox(allAppsContainer); + final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null; int deviceHeight = mLauncher.getRealDisplaySize().y; int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen(); @@ -133,8 +128,10 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { mLauncher.getVisibleBounds(icon).top < bottomGestureStartOnScreen) .collect(Collectors.toList()), - mLauncher.getVisibleBounds(searchBox).bottom - - mLauncher.getVisibleBounds(allAppsContainer).top); + hasSearchBox() + ? mLauncher.getVisibleBounds(searchBox).bottom + - mLauncher.getVisibleBounds(allAppsContainer).top + : 0); verifyActiveContainer(); final int newScroll = getAllAppsScroll(); mLauncher.assertTrue( @@ -150,29 +147,49 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { } verifyActiveContainer(); } - // Ignore bottom offset selection here as there might not be any scroll more scroll // region available. - mLauncher.assertTrue("Unable to scroll to a clickable icon: " + appName, - hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector, - deviceHeight)); + if (hasClickableIcon( + allAppsContainer, appListRecycler, appIconSelector, deviceHeight)) { - final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler, - appIconSelector); - return new AppIcon(mLauncher, appIcon); + final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler, + appIconSelector); + return createAppIcon(appIcon); + } else { + return null; + } } } + /** + * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make + * sure the icon is visible. + * + * @param appName name of the app. + * @return The app. + */ + @NonNull + public AppIcon getAppIcon(String appName) { + AppIcon appIcon = tryGetAppIcon(appName); + mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon); + return appIcon; + } + + @NonNull + protected abstract AppIcon createAppIcon(UiObject2 icon); + + protected abstract boolean hasSearchBox(); + private void scrollBackToBeginning() { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to scroll back in all apps")) { LauncherInstrumentation.log("Scrolling to the beginning"); final UiObject2 allAppsContainer = verifyActiveContainer(); - final UiObject2 searchBox = getSearchBox(allAppsContainer); + final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null; int attempts = 0; - final Rect margins = - new Rect(0, mLauncher.getVisibleBounds(searchBox).bottom + 1, 0, 5); + final Rect margins = new Rect( + 0, hasSearchBox() ? mLauncher.getVisibleBounds(searchBox).bottom + 1 : 0, 0, 5); for (int scroll = getAllAppsScroll(); scroll != 0; @@ -184,7 +201,11 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { ++attempts <= MAX_SCROLL_ATTEMPTS); mLauncher.scroll( - allAppsContainer, Direction.UP, margins, 12, false); + allAppsContainer, + Direction.UP, + margins, + /* steps= */ 12, + /* slowDown= */ false); } try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled up")) { @@ -213,7 +234,11 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { final UiObject2 allAppsContainer = verifyActiveContainer(); // Start the gesture in the center to avoid starting at elements near the top. mLauncher.scroll( - allAppsContainer, Direction.DOWN, new Rect(0, 0, 0, mHeight / 2), 10, false); + allAppsContainer, + Direction.DOWN, + new Rect(0, 0, 0, mHeight / 2), + /* steps= */ 10, + /* slowDown= */ false); verifyActiveContainer(); } } @@ -228,7 +253,11 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { final UiObject2 allAppsContainer = verifyActiveContainer(); // Start the gesture in the center, for symmetry with forward. mLauncher.scroll( - allAppsContainer, Direction.UP, new Rect(0, mHeight / 2, 0, 0), 10, false); + allAppsContainer, + Direction.UP, + new Rect(0, mHeight / 2, 0, 0), + /* steps= */ 10, + /*slowDown= */ false); verifyActiveContainer(); } } @@ -253,4 +282,4 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { if (testInfo == null) return; mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD)); } -} +} \ No newline at end of file diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java b/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java new file mode 100644 index 0000000000..8adce29436 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +import java.util.regex.Pattern; + +/** + * App icon in all apps. + */ +final class AllAppsAppIcon extends HomeAppIcon { + + private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick"); + + AllAppsAppIcon(LauncherInstrumentation launcher, UiObject2 icon) { + super(launcher, icon); + } + + @Override + protected Pattern getLongClickEvent() { + return LONG_CLICK_EVENT; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java deleted file mode 100644 index 835790ddff..0000000000 --- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.tapl; - -import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL; - -import android.graphics.Point; - -import androidx.annotation.NonNull; -import androidx.test.uiautomator.UiObject2; - -import com.android.launcher3.testing.TestProtocol; - -/** - * Operations on AllApps opened from Overview. - */ -public final class AllAppsFromOverview extends AllApps { - - AllAppsFromOverview(LauncherInstrumentation launcher) { - super(launcher); - verifyActiveContainer(); - } - - /** - * Swipes down to switch back to Overview whence we came from. - * - * @return the overview panel. - */ - @NonNull - public Overview switchBackToOverview() { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); - LauncherInstrumentation.Closable c = mLauncher.addContextLayer( - "want to switch back from all apps to overview")) { - final UiObject2 allAppsContainer = verifyActiveContainer(); - // Swipe from the search box to the bottom. - final UiObject2 qsb = mLauncher.waitForObjectInContainer( - allAppsContainer, "search_container_all_apps"); - final Point start = qsb.getVisibleCenter(); - final int swipeHeight = mLauncher.getTestInfo( - TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT). - getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); - - final int endY = start.y + swipeHeight; - LauncherInstrumentation.log("AllAppsFromOverview.switchBackToOverview before swipe"); - mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL, - LauncherInstrumentation.GestureScope.INSIDE); - - try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("swiped down")) { - return new Overview(mLauncher); - } - } - } -} diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java new file mode 100644 index 0000000000..516402563d --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; + +/** + * Operations on AllApps opened from the Taskbar. + */ +public class AllAppsFromTaskbar extends AllApps { + + AllAppsFromTaskbar(LauncherInstrumentation launcher) { + super(launcher); + } + + @Override + protected LauncherInstrumentation.ContainerType getContainerType() { + return LauncherInstrumentation.ContainerType.TASKBAR_ALL_APPS; + } + + @NonNull + @Override + public TaskbarAppIcon getAppIcon(String appName) { + return (TaskbarAppIcon) super.getAppIcon(appName); + } + + @NonNull + @Override + protected TaskbarAppIcon createAppIcon(UiObject2 icon) { + return new TaskbarAppIcon(mLauncher, icon); + } + + @Override + protected boolean hasSearchBox() { + return false; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java index 6da59da66c..e28f0af1bc 100644 --- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java @@ -16,11 +16,8 @@ package com.android.launcher3.tapl; -import android.graphics.Point; -import android.graphics.Rect; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiObject2; @@ -30,11 +27,9 @@ import com.android.launcher3.testing.TestProtocol; import java.util.regex.Pattern; /** - * App icon, whether in all apps or in workspace/ + * App icon, whether in all apps, workspace or the taskbar. */ -public final class AppIcon extends Launchable implements FolderDragTarget { - - private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick"); +public abstract class AppIcon extends Launchable { AppIcon(LauncherInstrumentation launcher, UiObject2 icon) { super(launcher, icon); @@ -44,13 +39,19 @@ public final class AppIcon extends Launchable implements FolderDragTarget { return By.clazz(TextView.class).text(appName).pkg(launcher.getLauncherPackageName()); } + static BySelector getAnyAppIconSelector() { + return By.clazz(TextView.class); + } + + protected abstract Pattern getLongClickEvent(); + /** * Long-clicks the icon to open its menu. */ public AppIconMenu openMenu() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - return new AppIconMenu(mLauncher, mLauncher.clickAndGet( - mObject, "popup_container", LONG_CLICK_EVENT)); + return createMenu(mLauncher.clickAndGet( + mObject, /* resName= */ "popup_container", getLongClickEvent())); } } @@ -59,42 +60,21 @@ public final class AppIcon extends Launchable implements FolderDragTarget { */ public AppIconMenu openDeepShortcutMenu() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - return new AppIconMenu(mLauncher, mLauncher.clickAndGet( - mObject, "deep_shortcuts_container", LONG_CLICK_EVENT)); + return createMenu(mLauncher.clickAndGet( + mObject, /* resName= */ "deep_shortcuts_container", getLongClickEvent())); } } - /** - * Drag the AppIcon to the given position of other icon. The drag must result in a folder. - * - * @param target the destination icon. - */ - @NonNull - public FolderIcon dragToIcon(FolderDragTarget target) { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); - LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to drag icon")) { - final Rect dropBounds = target.getDropLocationBounds(); - Workspace.dragIconToWorkspace( - mLauncher, this, - () -> { - final Rect bounds = target.getDropLocationBounds(); - return new Point(bounds.centerX(), bounds.centerY()); - }, - getLongPressIndicator()); - FolderIcon result = target.getTargetFolder(dropBounds); - mLauncher.assertTrue("Can't find the target folder.", result != null); - return result; - } - } + protected abstract AppIconMenu createMenu(UiObject2 menu); @Override protected void addExpectedEventsForLongClick() { - mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT); + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, getLongClickEvent()); } @Override - protected String getLongPressIndicator() { - return "popup_container"; + protected void waitForLongPressConfirmation() { + mLauncher.waitForLauncherObject("popup_container"); } @Override @@ -106,20 +86,4 @@ public final class AppIcon extends Launchable implements FolderDragTarget { protected String launchableType() { return "app icon"; } - - @Override - public Rect getDropLocationBounds() { - return mLauncher.getVisibleBounds(mObject); - } - - @Override - public FolderIcon getTargetFolder(Rect bounds) { - for (FolderIcon folderIcon : mLauncher.getWorkspace().getFolderIcons()) { - final Rect folderIconBounds = folderIcon.getDropLocationBounds(); - if (bounds.contains(folderIconBounds.centerX(), folderIconBounds.centerY())) { - return folderIcon; - } - } - return null; - } } diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java index 7f28151b62..82d96309f1 100644 --- a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java +++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java @@ -25,9 +25,9 @@ import java.util.List; /** * Context menu of an app icon. */ -public class AppIconMenu { - private final LauncherInstrumentation mLauncher; - private final UiObject2 mDeepShortcutsContainer; +public abstract class AppIconMenu { + protected final LauncherInstrumentation mLauncher; + protected final UiObject2 mDeepShortcutsContainer; AppIconMenu(LauncherInstrumentation launcher, UiObject2 deepShortcutsContainer) { @@ -42,6 +42,17 @@ public class AppIconMenu { final List menuItems = mLauncher.getObjectsInContainer(mDeepShortcutsContainer, "bubble_text"); assertTrue(menuItems.size() > itemNumber); - return new AppIconMenuItem(mLauncher, menuItems.get(itemNumber)); + return createMenuItem(menuItems.get(itemNumber)); } + + /** + * Returns a menu item with the given text. Fails if it doesn't exist. + */ + public AppIconMenuItem getMenuItem(String shortcutText) { + final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer, + AppIcon.getAppIconSelector(shortcutText, mLauncher)); + return createMenuItem(menuItem); + } + + protected abstract AppIconMenuItem createMenuItem(UiObject2 menuItem); } diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java index ac0db0876c..5cf5abab0b 100644 --- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java +++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java @@ -23,7 +23,7 @@ import com.android.launcher3.testing.TestProtocol; /** * Menu item in an app icon menu. */ -public class AppIconMenuItem extends Launchable { +public abstract class AppIconMenuItem extends Launchable { AppIconMenuItem(LauncherInstrumentation launcher, UiObject2 shortcut) { super(launcher, shortcut); @@ -41,8 +41,8 @@ public class AppIconMenuItem extends Launchable { } @Override - protected String getLongPressIndicator() { - return "drop_target_bar"; + protected void waitForLongPressConfirmation() { + mLauncher.waitForLauncherObject("drop_target_bar"); } @Override diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java index 4eaecca9fb..589e13cab5 100644 --- a/tests/tapl/com/android/launcher3/tapl/Background.java +++ b/tests/tapl/com/android/launcher3/tapl/Background.java @@ -37,7 +37,7 @@ import java.util.regex.Pattern; * Indicates the base state with a UI other than Overview running as foreground. It can also * indicate Launcher as long as Launcher is not in Overview state. */ -public class Background extends LauncherInstrumentation.VisibleContainer { +public abstract class Background extends LauncherInstrumentation.VisibleContainer { private static final int ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION = 500; private static final Pattern SQUARE_BUTTON_EVENT = Pattern.compile("onOverviewToggle"); @@ -45,11 +45,6 @@ public class Background extends LauncherInstrumentation.VisibleContainer { super(launcher); } - @Override - protected LauncherInstrumentation.ContainerType getContainerType() { - return LauncherInstrumentation.ContainerType.BACKGROUND; - } - /** * Swipes up or presses the square button to switch to Overview. * Returns the base overview, which can be either in Launcher or the fallback recents. @@ -80,7 +75,8 @@ public class Background extends LauncherInstrumentation.VisibleContainer { protected void goToOverviewUnchecked() { switch (mLauncher.getNavigationModel()) { case ZERO_BUTTON: { - sendDownPointerToEnterOverviewToLauncher(); + final long downTime = SystemClock.uptimeMillis(); + sendDownPointerToEnterOverviewToLauncher(downTime); String swipeAndHoldToEnterOverviewActionName = "swiping and holding to enter overview"; // If swiping from an app (e.g. Overview is in Background), we pause and hold on @@ -89,16 +85,17 @@ public class Background extends LauncherInstrumentation.VisibleContainer { // Workspace state where the below condition is true), there is no need to pause, // and we will not test for an intermediate carousel as one will not exist. if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) { - mLauncher.runToState(this::sendSwipeUpAndHoldToEnterOverviewGestureToLauncher, + mLauncher.runToState( + () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime), OVERVIEW_STATE_ORDINAL, swipeAndHoldToEnterOverviewActionName); - sendUpPointerToEnterOverviewToLauncher(); + sendUpPointerToEnterOverviewToLauncher(downTime); } else { // If swiping up from an app to overview, pause on intermediate carousel // until snapshots are visible. No intermediate carousel when swiping from // Home. The task swiped up is not a snapshot but the TaskViewSimulator. If // only a single task exists, no snapshots will be available during swipe up. mLauncher.executeAndWaitForLauncherEvent( - this::sendSwipeUpAndHoldToEnterOverviewGestureToLauncher, + () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime), event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals( event.getClassName().toString()), () -> "Pause wasn't detected", @@ -127,38 +124,13 @@ public class Background extends LauncherInstrumentation.VisibleContainer { } String upPointerToEnterOverviewActionName = "sending UP pointer to enter overview"; - mLauncher.runToState(this::sendUpPointerToEnterOverviewToLauncher, + mLauncher.runToState(() -> sendUpPointerToEnterOverviewToLauncher(downTime), OVERVIEW_STATE_ORDINAL, upPointerToEnterOverviewActionName); } } break; } - case TWO_BUTTON: { - final int startX; - final int startY; - final int endX; - final int endY; - final int swipeLength = mLauncher.getTestInfo(getSwipeHeightRequestName()). - getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) + mLauncher.getTouchSlop(); - - if (mLauncher.getDevice().isNaturalOrientation()) { - startX = endX = mLauncher.getDevice().getDisplayWidth() / 2; - startY = getSwipeStartY(); - endY = startY - swipeLength; - } else { - startX = getSwipeStartX(); - // TODO(b/184059820) make horizontal swipe use swipe width not height, for the - // moment just double the swipe length. - endX = startX - swipeLength * 2; - startY = endY = mLauncher.getDevice().getDisplayHeight() / 2; - } - - mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL, - LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER); - break; - } - case THREE_BUTTON: if (mLauncher.isTablet()) { mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, @@ -178,21 +150,24 @@ public class Background extends LauncherInstrumentation.VisibleContainer { private void expectSwitchToOverviewEvents() { } - private void sendDownPointerToEnterOverviewToLauncher() { + private void sendDownPointerToEnterOverviewToLauncher(long downTime) { final int centerX = mLauncher.getDevice().getDisplayWidth() / 2; final int startY = getSwipeStartY(); final Point start = new Point(centerX, startY); - final long downTime = SystemClock.uptimeMillis(); final LauncherInstrumentation.GestureScope gestureScope = zeroButtonToOverviewGestureStartsInLauncher() ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER; - mLauncher.sendPointer( - downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope); + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope); + + if (!mLauncher.isLauncher3()) { + mLauncher.expectEvent(TestProtocol.SEQUENCE_PILFER, + LauncherInstrumentation.EVENT_PILFER_POINTERS); + } } - private void sendSwipeUpAndHoldToEnterOverviewGestureToLauncher() { + private void sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(long downTime) { final int centerX = mLauncher.getDevice().getDisplayWidth() / 2; final int startY = getSwipeStartY(); final int swipeHeight = mLauncher.getTestInfo(getSwipeHeightRequestName()).getInt( @@ -200,7 +175,6 @@ public class Background extends LauncherInstrumentation.VisibleContainer { final Point start = new Point(centerX, startY); final Point end = new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop()); - final long downTime = SystemClock.uptimeMillis(); final LauncherInstrumentation.GestureScope gestureScope = zeroButtonToOverviewGestureStartsInLauncher() ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE @@ -215,35 +189,35 @@ public class Background extends LauncherInstrumentation.VisibleContainer { gestureScope); } - private void sendUpPointerToEnterOverviewToLauncher() { + private void sendUpPointerToEnterOverviewToLauncher(long downTime) { final int centerX = mLauncher.getDevice().getDisplayWidth() / 2; final int startY = getSwipeStartY(); final int swipeHeight = mLauncher.getTestInfo(getSwipeHeightRequestName()).getInt( TestProtocol.TEST_INFO_RESPONSE_FIELD); final Point end = new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop()); - final long downTime = SystemClock.uptimeMillis(); + final LauncherInstrumentation.GestureScope gestureScope = zeroButtonToOverviewGestureStartsInLauncher() - ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE - : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER; + ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER + : LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER; mLauncher.sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end, gestureScope); } @NonNull - public Background quickSwitchToPreviousApp() { + public LaunchedAppState quickSwitchToPreviousApp() { boolean toRight = true; quickSwitch(toRight); - return new Background(mLauncher); + return new LaunchedAppState(mLauncher); } @NonNull - public Background quickSwitchToPreviousAppSwipeLeft() { + public LaunchedAppState quickSwitchToPreviousAppSwipeLeft() { boolean toRight = false; quickSwitch(toRight); - return new Background(mLauncher); + return new LaunchedAppState(mLauncher); } @NonNull @@ -253,11 +227,7 @@ public class Background extends LauncherInstrumentation.VisibleContainer { "want to quick switch to the previous app")) { verifyActiveContainer(); final boolean launcherWasVisible = mLauncher.isLauncherVisible(); - boolean transposeInLandscape = false; switch (mLauncher.getNavigationModel()) { - case TWO_BUTTON: - transposeInLandscape = true; - // Fall through, zero button and two button modes behave the same. case ZERO_BUTTON: { final int startX; final int startY; @@ -265,33 +235,17 @@ public class Background extends LauncherInstrumentation.VisibleContainer { final int endY; final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius()); if (toRight) { - if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) { - // Swipe from the bottom left to the bottom right of the screen. - startX = cornerRadius; - startY = getSwipeStartY(); - endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius; - endY = startY; - } else { - // Swipe from the bottom right to the top right of the screen. - startX = getSwipeStartX(); - startY = mLauncher.getRealDisplaySize().y - 1 - cornerRadius; - endX = startX; - endY = cornerRadius; - } + // Swipe from the bottom left to the bottom right of the screen. + startX = cornerRadius; + startY = getSwipeStartY(); + endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius; + endY = startY; } else { - if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) { - // Swipe from the bottom right to the bottom left of the screen. - startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius; - startY = getSwipeStartY(); - endX = cornerRadius; - endY = startY; - } else { - // Swipe from the bottom left to the top left of the screen. - startX = getSwipeStartX(); - startY = cornerRadius; - endX = startX; - endY = mLauncher.getRealDisplaySize().y - 1 - cornerRadius; - } + // Swipe from the bottom right to the bottom left of the screen. + startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius; + startY = getSwipeStartY(); + endX = cornerRadius; + endY = startY; } final boolean isZeroButton = mLauncher.getNavigationModel() diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java index 3eb8cf1096..b7bca02e44 100644 --- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java +++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java @@ -127,7 +127,8 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer { OverviewTask task = getCurrentTask(); mLauncher.assertNotNull("current task is null", task); - mLauncher.scrollLeftByDistance(verifyActiveContainer(), task.getVisibleWidth()); + mLauncher.scrollLeftByDistance(verifyActiveContainer(), + task.getVisibleWidth() + mLauncher.getOverviewPageSpacing()); try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("scrolled task off screen")) { diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java index dba308dbf7..26f0a8b26d 100644 --- a/tests/tapl/com/android/launcher3/tapl/Folder.java +++ b/tests/tapl/com/android/launcher3/tapl/Folder.java @@ -40,10 +40,10 @@ public class Folder { * Find an app icon with given name or raise assertion error. */ @NonNull - public AppIcon getAppIcon(String appName) { + public HomeAppIcon getAppIcon(String appName) { try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer( "Want to get app icon in folder")) { - return new AppIcon(mLauncher, + return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer( mContainer, AppIcon.getAppIconSelector(appName, mLauncher))); diff --git a/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java b/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java index d797418ab4..2c60668ba8 100644 --- a/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java +++ b/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java @@ -19,7 +19,10 @@ package com.android.launcher3.tapl; import android.graphics.Rect; public interface FolderDragTarget { + + /** This method requires public access, however should not be called in tests. */ Rect getDropLocationBounds(); + /** This method requires public access, however should not be called in tests. */ FolderIcon getTargetFolder(Rect bounds); } diff --git a/tests/tapl/com/android/launcher3/tapl/FolderIcon.java b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java index 2e79d70ee6..9b4717fe12 100644 --- a/tests/tapl/com/android/launcher3/tapl/FolderIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java @@ -52,11 +52,13 @@ public class FolderIcon implements FolderDragTarget { return new Folder(mLauncher); } + /** This method requires public access, however should not be called in tests. */ @Override public Rect getDropLocationBounds() { return mLauncher.getVisibleBounds(mObject.getParent()); } + /** This method requires public access, however should not be called in tests. */ @Override public FolderIcon getTargetFolder(Rect bounds) { return this; diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java new file mode 100644 index 0000000000..c275f3b320 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; + +public class HomeAllApps extends AllApps { + + HomeAllApps(LauncherInstrumentation launcher) { + super(launcher); + } + + @Override + protected LauncherInstrumentation.ContainerType getContainerType() { + return LauncherInstrumentation.ContainerType.HOME_ALL_APPS; + } + + @NonNull + @Override + public HomeAppIcon getAppIcon(String appName) { + return (AllAppsAppIcon) super.getAppIcon(appName); + } + + @NonNull + @Override + protected HomeAppIcon createAppIcon(UiObject2 icon) { + return new AllAppsAppIcon(mLauncher, icon); + } + + @Override + protected boolean hasSearchBox() { + return true; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java b/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java new file mode 100644 index 0000000000..71d8ba9a41 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import android.graphics.Point; +import android.graphics.Rect; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; + +import java.util.function.Supplier; + +/** + * App icon on the workspace or all apps. + */ +public abstract class HomeAppIcon extends AppIcon implements FolderDragTarget, WorkspaceDragSource { + + private final String mAppName; + + HomeAppIcon(LauncherInstrumentation launcher, UiObject2 icon) { + super(launcher, icon); + mAppName = icon.getText(); + } + + /** + * Drag the AppIcon to the given position of other icon. The drag must result in a folder. + * + * @param target the destination icon. + */ + @NonNull + public FolderIcon dragToIcon(FolderDragTarget target) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to drag icon")) { + final Rect dropBounds = target.getDropLocationBounds(); + Workspace.dragIconToWorkspace( + mLauncher, this, + () -> { + final Rect bounds = target.getDropLocationBounds(); + return new Point(bounds.centerX(), bounds.centerY()); + }); + FolderIcon result = target.getTargetFolder(dropBounds); + mLauncher.assertTrue("Can't find the target folder.", result != null); + return result; + } + } + + /** This method requires public access, however should not be called in tests. */ + @Override + public Rect getDropLocationBounds() { + return mLauncher.getVisibleBounds(mObject); + } + + /** This method requires public access, however should not be called in tests. */ + @Override + public FolderIcon getTargetFolder(Rect bounds) { + for (FolderIcon folderIcon : mLauncher.getWorkspace().getFolderIcons()) { + final Rect folderIconBounds = folderIcon.getDropLocationBounds(); + if (bounds.contains(folderIconBounds.centerX(), folderIconBounds.centerY())) { + return folderIcon; + } + } + return null; + } + + @Override + public HomeAppIconMenu openDeepShortcutMenu() { + return (HomeAppIconMenu) super.openDeepShortcutMenu(); + } + + @Override + protected HomeAppIconMenu createMenu(UiObject2 menu) { + return new HomeAppIconMenu(mLauncher, menu); + } + + /** + * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar. + * + * @return validated workspace after the existing appIcon being uninstalled. + */ + public Workspace uninstall() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "uninstalling app icon")) { + return Workspace.uninstallAppIcon( + mLauncher, this, + this::addExpectedEventsForLongClick + ); + } + } + + /** + * Drag an object to the given cell in workspace. The target cell must be empty. + * + * @param cellX zero based column number, starting from the left of the screen. + * @param cellY zero based row number, starting from the top of the screen. + */ + public HomeAppIcon dragToWorkspace(int cellX, int cellY) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + String.format("want to drag the icon to cell(%d, %d)", cellX, cellY)) + ) { + final Supplier dest = () -> Workspace.getCellCenter(mLauncher, cellX, cellY); + Workspace.dragIconToWorkspace( + mLauncher, + /* launchable= */ this, + dest, + () -> addExpectedEventsForLongClick(), + /*expectDropEvents= */ null); + try (LauncherInstrumentation.Closable ignore = mLauncher.addContextLayer("dragged")) { + WorkspaceAppIcon appIcon = + (WorkspaceAppIcon) mLauncher.getWorkspace().getWorkspaceAppIcon(mAppName); + mLauncher.assertTrue( + String.format( + "The %s icon should be in the cell (%d, %d).", mAppName, cellX, + cellY), + appIcon.isInCell(cellX, cellY)); + return appIcon; + } + } + } + + + /** This method requires public access, however should not be called in tests. */ + @Override + public Launchable getLaunchable() { + return this; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenu.java new file mode 100644 index 0000000000..71fb6c0841 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenu.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +/** + * Context menu of a home screen app icon. + */ +public final class HomeAppIconMenu extends AppIconMenu { + + HomeAppIconMenu(LauncherInstrumentation launcher, + UiObject2 deepShortcutsContainer) { + super(launcher, deepShortcutsContainer); + } + + @Override + public HomeAppIconMenuItem getMenuItem(int itemNumber) { + return (HomeAppIconMenuItem) super.getMenuItem(itemNumber); + } + + @Override + protected HomeAppIconMenuItem createMenuItem(UiObject2 menuItem) { + return new HomeAppIconMenuItem(mLauncher, menuItem); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenuItem.java new file mode 100644 index 0000000000..1ff0c10506 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/HomeAppIconMenuItem.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +/** + * Menu item in a home screen app icon menu. + */ +public final class HomeAppIconMenuItem extends AppIconMenuItem implements WorkspaceDragSource { + + HomeAppIconMenuItem(LauncherInstrumentation launcher, + UiObject2 shortcut) { + super(launcher, shortcut); + } + + /** This method requires public access, however should not be called in tests. */ + @Override + public Launchable getLaunchable() { + return this; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java new file mode 100644 index 0000000000..5f921996cd --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +/** + * Operations on home screen qsb. + */ +public class HomeQsb { + + private final LauncherInstrumentation mLauncher; + + HomeQsb(LauncherInstrumentation launcher) { + mLauncher = launcher; + mLauncher.waitForLauncherObject("search_container_hotseat"); + } + + /** + * Show search result page from tapping qsb. + */ + public SearchResultFromQsb showSearchResult() { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to open search result page"); + LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + mLauncher.clickLauncherObject( + mLauncher.waitForLauncherObject("search_container_hotseat")); + try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer( + "clicked qsb to open search result page")) { + return new SearchResultFromQsb(mLauncher); + } + } + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java index 7ec52084aa..33fea2d10d 100644 --- a/tests/tapl/com/android/launcher3/tapl/Launchable.java +++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java @@ -16,19 +16,25 @@ package com.android.launcher3.tapl; -import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; +import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL; import android.graphics.Point; +import android.view.MotionEvent; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.Until; +import com.android.launcher3.testing.TestProtocol; + /** * Ancestor for AppIcon and AppMenuItem. */ -abstract class Launchable { +public abstract class Launchable { + + protected static final int DEFAULT_DRAG_STEPS = 10; + protected final LauncherInstrumentation mLauncher; protected final UiObject2 mObject; @@ -45,7 +51,7 @@ abstract class Launchable { /** * Clicks the object to launch its app. */ - public Background launch(String expectedPackageName) { + public LaunchedAppState launch(String expectedPackageName) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { return launch(By.pkg(expectedPackageName)); } @@ -55,59 +61,96 @@ abstract class Launchable { protected abstract String launchableType(); - private Background launch(BySelector selector) { - try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + private LaunchedAppState launch(BySelector selector) { + try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "want to launch an app from " + launchableType())) { LauncherInstrumentation.log("Launchable.launch before click " + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject)); - final String label = mObject.getText(); - mLauncher.executeAndWaitForEvent( - () -> { - mLauncher.clickLauncherObject(mObject); - expectActivityStartEvents(); - }, - event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED, - () -> "Launching an app didn't open a new window: " + label, - "clicking " + launchableType()); + mLauncher.clickLauncherObject(mObject); - try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("clicked")) { - mLauncher.assertTrue( - "App didn't start: " + label + " (" + selector + ")", - TestHelpers.wait(Until.hasObject(selector), - LauncherInstrumentation.WAIT_TIME_MS)); - return new Background(mLauncher); + try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) { + expectActivityStartEvents(); + return assertAppLaunched(selector); } } } - /** - * Drags an object to the center of homescreen. - * - * @param startsActivity whether it's expected to start an activity. - * @param isWidgetShortcut whether we drag a widget shortcut - */ - public void dragToWorkspace(boolean startsActivity, boolean isWidgetShortcut) { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - final Point launchableCenter = getObject().getVisibleCenter(); - final Point displaySize = mLauncher.getRealDisplaySize(); - final int width = displaySize.x / 2; - Workspace.dragIconToWorkspace( - mLauncher, - this, - new Point( - launchableCenter.x >= width - ? launchableCenter.x - width / 2 - : launchableCenter.x + width / 2, - displaySize.y / 2), - getLongPressIndicator(), - startsActivity, - isWidgetShortcut, - () -> addExpectedEventsForLongClick()); + protected LaunchedAppState assertAppLaunched(BySelector selector) { + mLauncher.assertTrue( + "App didn't start: (" + selector + ")", + mLauncher.getDevice().wait(Until.hasObject(selector), + LauncherInstrumentation.WAIT_TIME_MS)); + return new LaunchedAppState(mLauncher); + } + + Point startDrag(long downTime, Runnable expectLongClickEvents, boolean runToSpringLoadedState) { + final Point iconCenter = getObject().getVisibleCenter(); + final Point dragStartCenter = new Point(iconCenter.x, + iconCenter.y - getStartDragThreshold()); + + if (runToSpringLoadedState) { + mLauncher.runToState(() -> movePointerForStartDrag( + downTime, + iconCenter, + dragStartCenter, + expectLongClickEvents), + SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start"); + } else { + movePointerForStartDrag( + downTime, + iconCenter, + dragStartCenter, + expectLongClickEvents); } + + return dragStartCenter; + } + + /** + * Waits for a confirmation that a long press has successfully been triggered. + * + * This method waits for a view to either appear or disappear to confirm that the long press + * has been triggered and fails if no confirmation is received before the default timeout. + */ + protected abstract void waitForLongPressConfirmation(); + + /** + * Drags this Launchable a short distance before starting a full drag. + * + * This is necessary for shortcuts, which require being dragged beyond a threshold to close + * their container and start drag callbacks. + */ + private void movePointerForStartDrag( + long downTime, + Point iconCenter, + Point dragStartCenter, + Runnable expectLongClickEvents) { + mLauncher.sendPointer( + downTime, + downTime, + MotionEvent.ACTION_DOWN, + iconCenter, + LauncherInstrumentation.GestureScope.INSIDE); + LauncherInstrumentation.log("movePointerForStartDrag: sent down"); + expectLongClickEvents.run(); + waitForLongPressConfirmation(); + LauncherInstrumentation.log("movePointerForStartDrag: indicator"); + mLauncher.movePointer( + iconCenter, + dragStartCenter, + DEFAULT_DRAG_STEPS, + /* isDecelerating= */ false, + downTime, + downTime, + /* slowDown= */ true, + LauncherInstrumentation.GestureScope.INSIDE); + } + + private int getStartDragThreshold() { + return mLauncher.getTestInfo(TestProtocol.REQUEST_START_DRAG_THRESHOLD).getInt( + TestProtocol.TEST_INFO_RESPONSE_FIELD); } protected abstract void addExpectedEventsForLongClick(); - - protected abstract String getLongPressIndicator(); } diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java new file mode 100644 index 0000000000..046d36b0ba --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import static com.android.launcher3.testing.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING; +import static com.android.launcher3.testing.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING; +import static com.android.launcher3.testing.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT; + +import android.graphics.Point; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import androidx.test.uiautomator.By; + +import com.android.launcher3.testing.TestProtocol; + +/** + * Background state operations specific to when an app has been launched. + */ +public final class LaunchedAppState extends Background { + + // More drag steps than Launchables to give the window manager time to register the drag. + private static final int DEFAULT_DRAG_STEPS = 35; + + LaunchedAppState(LauncherInstrumentation launcher) { + super(launcher); + } + + @Override + protected LauncherInstrumentation.ContainerType getContainerType() { + return LauncherInstrumentation.ContainerType.LAUNCHED_APP; + } + + /** + * Returns the taskbar. + * + * The taskbar must already be visible when calling this method. + */ + public Taskbar getTaskbar() { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to get the taskbar")) { + mLauncher.waitForLauncherObject("taskbar_view"); + + return new Taskbar(mLauncher); + } + } + + /** + * Returns the Taskbar in a visible state. + * + * The taskbar must already be hidden when calling this method. + */ + public Taskbar showTaskbar() { + mLauncher.getTestInfo(REQUEST_ENABLE_MANUAL_TASKBAR_STASHING); + + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( + "want to show the taskbar")) { + mLauncher.waitUntilLauncherObjectGone("taskbar_view"); + + final long downTime = SystemClock.uptimeMillis(); + final int unstashTargetY = mLauncher.getRealDisplaySize().y + - (mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_HEIGHT) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) / 2); + final Point unstashTarget = new Point( + mLauncher.getRealDisplaySize().x / 2, unstashTargetY); + + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, unstashTarget, + LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER); + LauncherInstrumentation.log("showTaskbar: sent down"); + + try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) { + mLauncher.waitForLauncherObject("taskbar_view"); + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, unstashTarget, + LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER); + + return new Taskbar(mLauncher); + } + } finally { + mLauncher.getTestInfo(REQUEST_DISABLE_MANUAL_TASKBAR_STASHING); + } + } + + static void dragToSplitscreen( + LauncherInstrumentation launcher, + Launchable launchable, + String expectedNewPackageName, + String expectedExistingPackageName) { + try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer( + "want to drag taskbar item to splitscreen")) { + final Point displaySize = launcher.getRealDisplaySize(); + // Drag to the center of the top-left quadrant of the screen, this point will work in + // both portrait and landscape. + final Point endPoint = new Point(displaySize.x / 4, displaySize.y / 4); + final long downTime = SystemClock.uptimeMillis(); + // Use mObject before starting drag since the system drag and drop moves the original + // view. + Point itemVisibleCenter = launchable.mObject.getVisibleCenter(); + Rect itemVisibleBounds = launcher.getVisibleBounds(launchable.mObject); + String itemLabel = launchable.mObject.getText(); + + Point dragStart = launchable.startDrag( + downTime, + launchable::addExpectedEventsForLongClick, + /* runToSpringLoadedState= */ false); + + try (LauncherInstrumentation.Closable c2 = launcher.addContextLayer( + "started item drag")) { + launcher.movePointer( + dragStart, + endPoint, + DEFAULT_DRAG_STEPS, + /* isDecelerating= */ true, + downTime, + SystemClock.uptimeMillis(), + /* slowDown= */ false, + LauncherInstrumentation.GestureScope.INSIDE); + + try (LauncherInstrumentation.Closable c3 = launcher.addContextLayer( + "moved pointer to drop point")) { + LauncherInstrumentation.log("SplitscreenDragSource.dragToSplitscreen: " + + "before drop " + itemVisibleCenter + " in " + itemVisibleBounds); + launcher.sendPointer( + downTime, + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, + endPoint, + LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER); + LauncherInstrumentation.log("SplitscreenDragSource.dragToSplitscreen: " + + "after drop"); + + try (LauncherInstrumentation.Closable c4 = launcher.addContextLayer( + "dropped item")) { + launchable.assertAppLaunched(By.pkg(expectedNewPackageName)); + launchable.assertAppLaunched(By.pkg(expectedExistingPackageName)); + } + } + } + } + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 631e8f10c6..9d25b1ba90 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -66,6 +66,7 @@ import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.Until; import com.android.launcher3.ResourceUtils; +import com.android.launcher3.testing.TestInformationRequest; import com.android.launcher3.testing.TestProtocol; import com.android.systemui.shared.system.ContextUtils; import com.android.systemui.shared.system.QuickStepContract; @@ -98,18 +99,19 @@ import java.util.stream.Collectors; public final class LauncherInstrumentation { private static final String TAG = "Tapl"; - private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20; + private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15; private static final int GESTURE_STEP_MS = 16; private static final long FORCE_PAUSE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(2); static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN"); static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP"); private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL"); - private static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers"); + static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers"); static final Pattern EVENT_START = Pattern.compile("start:"); static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN"); static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP"); + static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL"); static final Pattern EVENT_KEY_BACK_DOWN = getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK"); static final Pattern EVENT_KEY_BACK_UP = getKeyEventPattern("ACTION_UP", "KEYCODE_BACK"); @@ -121,20 +123,23 @@ public final class LauncherInstrumentation { // Types for launcher containers that the user is interacting with. "Background" is a // pseudo-container corresponding to inactive launcher covered by another app. public enum ContainerType { - WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, FALLBACK_OVERVIEW + WORKSPACE, HOME_ALL_APPS, OVERVIEW, WIDGETS, FALLBACK_OVERVIEW, LAUNCHED_APP, + TASKBAR_ALL_APPS } - public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON} + public enum NavigationModel {ZERO_BUTTON, THREE_BUTTON} // Where the gesture happens: outside of Launcher, inside or from inside to outside and // whether the gesture recognition triggers pilfer. public enum GestureScope { OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE, INSIDE_TO_OUTSIDE_WITHOUT_PILFER, + INSIDE_TO_OUTSIDE_WITH_KEYCODE, // For gestures that will trigger a keycode from TIS. + OUTSIDE_WITH_KEYCODE, } // Base class for launcher containers. - static abstract class VisibleContainer { + abstract static class VisibleContainer { protected final LauncherInstrumentation mLauncher; protected VisibleContainer(LauncherInstrumentation launcher) { @@ -165,6 +170,7 @@ public final class LauncherInstrumentation { private static final String OVERVIEW_RES_ID = "overview_panel"; private static final String WIDGETS_RES_ID = "primary_widgets_list_view"; private static final String CONTEXT_MENU_RES_ID = "popup_container"; + private static final String TASKBAR_RES_ID = "taskbar_view"; public static final int WAIT_TIME_MS = 60000; private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; private static final String ANDROID_PACKAGE = "android"; @@ -219,11 +225,6 @@ public final class LauncherInstrumentation { public LauncherInstrumentation(Instrumentation instrumentation) { mInstrumentation = instrumentation; mDevice = UiDevice.getInstance(instrumentation); - try { - mDevice.executeShellCommand("am wait-for-broadcast-idle"); - } catch (IOException e) { - log("Failed to wait for broadcast idle"); - } // Launcher should run in test harness so that custom accessibility protocol between // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call @@ -301,9 +302,13 @@ public final class LauncherInstrumentation { } Bundle getTestInfo(String request, String arg) { + return getTestInfo(request, arg, null); + } + + Bundle getTestInfo(String request, String arg, Bundle extra) { try (ContentProviderClient client = getContext().getContentResolver() .acquireContentProviderClient(mTestProviderUri)) { - return client.call(request, arg, null); + return client.call(request, arg, extra); } catch (DeadObjectException e) { fail("Launcher crashed"); return null; @@ -312,6 +317,12 @@ public final class LauncherInstrumentation { } } + Bundle getTestInfo(TestInformationRequest request) { + Bundle extra = new Bundle(); + extra.putParcelable(TestProtocol.TEST_INFO_REQUEST_FIELD, request); + return getTestInfo(request.getRequestName(), null, extra); + } + Insets getTargetInsets() { return getTestInfo(TestProtocol.REQUEST_TARGET_INSETS) .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD); @@ -342,6 +353,11 @@ public final class LauncherInstrumentation { .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD)); } + int getOverviewPageSpacing() { + return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_PAGE_SPACING) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + float getExactScreenCenterX() { return getRealDisplaySize().x / 2f; } @@ -386,8 +402,6 @@ public final class LauncherInstrumentation { public static NavigationModel getNavigationModel(int currentInteractionMode) { if (QuickStepContract.isGesturalMode(currentInteractionMode)) { return NavigationModel.ZERO_BUTTON; - } else if (QuickStepContract.isSwipeUpMode(currentInteractionMode)) { - return NavigationModel.TWO_BUTTON; } else if (QuickStepContract.isLegacyMode(currentInteractionMode)) { return NavigationModel.THREE_BUTTON; } @@ -421,18 +435,19 @@ public final class LauncherInstrumentation { } } - private String getSystemAnomalyMessage( + public String getSystemAnomalyMessage( boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) { try { { final StringBuilder sb = new StringBuilder(); - UiObject2 object = mDevice.findObject(By.res("android", "alertTitle")); + UiObject2 object = + mDevice.findObject(By.res("android", "alertTitle").pkg("android")); if (object != null) { sb.append("TITLE: ").append(object.getText()); } - object = mDevice.findObject(By.res("android", "message")); + object = mDevice.findObject(By.res("android", "message").pkg("android")); if (object != null) { sb.append(" PACKAGE: ").append(object.getApplicationPackage()) .append(" MESSAGE: ").append(object.getText()); @@ -511,7 +526,7 @@ public final class LauncherInstrumentation { if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview"; if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace"; if (hasLauncherObject(APPS_RES_ID)) return "AllApps"; - return "Background (" + getVisiblePackages() + ")"; + return "LaunchedApp (" + getVisiblePackages() + ")"; } public void setSystemHealthSupplier(Function supplier) { @@ -703,35 +718,54 @@ public final class LauncherInstrumentation { waitUntilLauncherObjectGone(APPS_RES_ID); waitUntilLauncherObjectGone(OVERVIEW_RES_ID); waitUntilLauncherObjectGone(WIDGETS_RES_ID); + waitUntilLauncherObjectGone(TASKBAR_RES_ID); + return waitForLauncherObject(WORKSPACE_RES_ID); } case WIDGETS: { waitUntilLauncherObjectGone(WORKSPACE_RES_ID); waitUntilLauncherObjectGone(APPS_RES_ID); waitUntilLauncherObjectGone(OVERVIEW_RES_ID); + waitUntilLauncherObjectGone(TASKBAR_RES_ID); + return waitForLauncherObject(WIDGETS_RES_ID); } - case ALL_APPS: { + case TASKBAR_ALL_APPS: + case HOME_ALL_APPS: { waitUntilLauncherObjectGone(WORKSPACE_RES_ID); waitUntilLauncherObjectGone(OVERVIEW_RES_ID); waitUntilLauncherObjectGone(WIDGETS_RES_ID); + waitUntilLauncherObjectGone(TASKBAR_RES_ID); + return waitForLauncherObject(APPS_RES_ID); } case OVERVIEW: { waitUntilLauncherObjectGone(APPS_RES_ID); waitUntilLauncherObjectGone(WORKSPACE_RES_ID); waitUntilLauncherObjectGone(WIDGETS_RES_ID); + waitUntilLauncherObjectGone(TASKBAR_RES_ID); return waitForLauncherObject(OVERVIEW_RES_ID); } case FALLBACK_OVERVIEW: { + waitUntilLauncherObjectGone(APPS_RES_ID); + waitUntilLauncherObjectGone(WORKSPACE_RES_ID); + waitUntilLauncherObjectGone(WIDGETS_RES_ID); + waitUntilLauncherObjectGone(TASKBAR_RES_ID); + return waitForFallbackLauncherObject(OVERVIEW_RES_ID); } - case BACKGROUND: { + case LAUNCHED_APP: { waitUntilLauncherObjectGone(WORKSPACE_RES_ID); waitUntilLauncherObjectGone(APPS_RES_ID); waitUntilLauncherObjectGone(OVERVIEW_RES_ID); waitUntilLauncherObjectGone(WIDGETS_RES_ID); + + if (isTablet() && !isFallbackOverview()) { + waitForLauncherObject(TASKBAR_RES_ID); + } else { + waitUntilLauncherObjectGone(TASKBAR_RES_ID); + } return null; } default: @@ -808,6 +842,8 @@ public final class LauncherInstrumentation { } GestureScope gestureScope = gestureStartFromLauncher + // Without the navigation bar layer, the gesture scope on tablets remains inside the + // launcher process. ? (isTablet() ? GestureScope.INSIDE : GestureScope.INSIDE_TO_OUTSIDE) : GestureScope.OUTSIDE_WITH_PILFER; linearGesture( @@ -823,12 +859,22 @@ public final class LauncherInstrumentation { } } + /** + * @return the Workspace object. + * @deprecated use goHome(). + * Presses nav bar home button. + */ + @Deprecated + public Workspace pressHome() { + return goHome(); + } + /** * Presses nav bar home button. * * @return the Workspace object. */ - public Workspace pressHome() { + public Workspace goHome() { try (LauncherInstrumentation.Closable e = eventsCheck(); LauncherInstrumentation.Closable c = addContextLayer("want to switch to home")) { waitForLauncherInitialized(); @@ -843,8 +889,13 @@ public final class LauncherInstrumentation { setForcePauseTimeout(FORCE_PAUSE_TIMEOUT_MS); final Point displaySize = getRealDisplaySize(); + // The swipe up to home gesture starts from inside the launcher when the user is + // already home. Otherwise, the gesture can start inside the launcher process if the + // taskbar is visible. boolean gestureStartFromLauncher = isTablet() - ? !isLauncher3() || hasLauncherObject(WORKSPACE_RES_ID) + ? !isLauncher3() + || hasLauncherObject(WORKSPACE_RES_ID) + || hasLauncherObject(TASKBAR_RES_ID) : isLauncherVisible(); // CLose floating views before going back to home. @@ -859,7 +910,7 @@ public final class LauncherInstrumentation { swipeToState( displaySize.x / 2, displaySize.y - 1, - displaySize.x / 2, 0, + displaySize.x / 2, displaySize.y / 2, ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL, gestureStartFromLauncher ? GestureScope.INSIDE_TO_OUTSIDE : GestureScope.OUTSIDE_WITH_PILFER); @@ -868,10 +919,6 @@ public final class LauncherInstrumentation { log("Hierarchy before clicking home:"); dumpViewHierarchy(); action = "clicking home button"; - if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) { - expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS); - expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS); - } if (isTablet()) { expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN); expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP); @@ -903,18 +950,17 @@ public final class LauncherInstrumentation { if (getNavigationModel() == NavigationModel.ZERO_BUTTON) { final Point displaySize = getRealDisplaySize(); final GestureScope gestureScope = - launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER - : GestureScope.OUTSIDE_WITHOUT_PILFER; - linearGesture(0, displaySize.y / 2, displaySize.x / 2, displaySize.y / 2, + launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE + : GestureScope.OUTSIDE_WITH_KEYCODE; + // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the + // issue is solved. + linearGesture(0, displaySize.y / 4, displaySize.x / 2, displaySize.y / 4, 10, false, gestureScope); } else { waitForNavigationUiObject("back").click(); if (isTablet()) { expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN); expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP); - } else if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) { - expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS); - expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS); } } if (launcherVisible) { @@ -952,14 +998,14 @@ public final class LauncherInstrumentation { } /** - * Gets the Workspace object if the current state is "background home", i.e. some other app is - * active. Fails if the launcher is not in that state. + * Gets the LaunchedApp object if another app is active. Fails if the launcher is not in that + * state. * - * @return Background object. + * @return LaunchedApp object. */ @NonNull - public Background getBackground() { - return new Background(this); + public LaunchedAppState getLaunchedAppState() { + return new LaunchedAppState(this); } /** @@ -996,32 +1042,17 @@ public final class LauncherInstrumentation { } /** - * Gets the All Apps object if the current state is showing the all apps panel opened by swiping - * from workspace. Fails if the launcher is not in that state. Please don't call this method if - * App Apps was opened by swiping up from Overview, as it won't fail and will return an - * incorrect object. + * Gets the homescreen All Apps object if the current state is showing the all apps panel opened + * by swiping from workspace. Fails if the launcher is not in that state. Please don't call this + * method if App Apps was opened by swiping up from Overview, as it won't fail and will return + * an incorrect object. * - * @return All Aps object. + * @return Home All Apps object. */ @NonNull - public AllApps getAllApps() { + public HomeAllApps getAllApps() { try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) { - return new AllApps(this); - } - } - - /** - * Gets the All Apps object if the current state is showing the all apps panel opened by swiping - * from overview. Fails if the launcher is not in that state. Please don't call this method if - * App Apps was opened by swiping up from home, as it won't fail and will return an - * incorrect object. - * - * @return All Aps object. - */ - @NonNull - public AllAppsFromOverview getAllAppsFromOverview() { - try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) { - return new AllAppsFromOverview(this); + return new HomeAllApps(this); } } @@ -1111,13 +1142,21 @@ public final class LauncherInstrumentation { @NonNull UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) { + return waitForObjectsInContainer(container, selector).get(0); + } + + @NonNull + List waitForObjectsInContainer( + UiObject2 container, BySelector selector) { try { - final UiObject2 object = container.wait( - Until.findObject(selector), + final List objects = container.wait( + Until.findObjects(selector), WAIT_TIME_MS); - assertNotNull("Can't find a view in Launcher, id: " + selector + " in container: " - + container.getResourceName(), object); - return object; + assertNotNull("Can't find views in Launcher, id: " + selector + " in container: " + + container.getResourceName(), objects); + assertTrue("Can't find views in Launcher, id: " + selector + " in container: " + + container.getResourceName(), objects.size() > 0); + return objects; } catch (StaleObjectException e) { fail("The container disappeared from screen"); return null; @@ -1276,13 +1315,11 @@ public final class LauncherInstrumentation { } int getRightGestureStartOnScreen() { - return getRealDisplaySize().x - getWindowInsets().right; + return getRealDisplaySize().x - getWindowInsets().right - 1; } - void clickLauncherObject(UiObject2 object) { - waitForObjectEnabled(object, "clickLauncherObject"); - expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN); - expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP); + void clickObject(UiObject2 object) { + waitForObjectEnabled(object, "clickObject"); if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) { expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS); expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS); @@ -1290,10 +1327,14 @@ public final class LauncherInstrumentation { object.click(); } + void clickLauncherObject(UiObject2 object) { + expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN); + expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP); + clickObject(object); + } + void scrollToLastVisibleRow( - UiObject2 container, - Collection items, - int topPaddingInContainer) { + UiObject2 container, Collection items, int topPaddingInContainer) { final UiObject2 lowestItem = Collections.max(items, (i1, i2) -> Integer.compare(getVisibleBounds(i1).top, getVisibleBounds(i2).top)); @@ -1316,21 +1357,19 @@ public final class LauncherInstrumentation { containerRect.height() - distance - bottomGestureMarginInContainer, 0, bottomGestureMarginInContainer), - 10, - true); + /* steps= */ 10, + /* slowDown= */ true); } void scrollLeftByDistance(UiObject2 container, int distance) { final Rect containerRect = getVisibleBounds(container); final int rightGestureMarginInContainer = getRightGestureMarginInContainer(container); + final int leftGestureMargin = getTargetInsets().left + getEdgeSensitivityWidth(); scroll( container, Direction.LEFT, - new Rect( - 0, - containerRect.width() - distance - rightGestureMarginInContainer, - 0, - rightGestureMarginInContainer), + new Rect(leftGestureMargin, 0, + containerRect.width() - distance - rightGestureMarginInContainer, 0), 10, true); } @@ -1400,13 +1439,13 @@ public final class LauncherInstrumentation { final Point end = new Point(endX, endY); sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope); final long endTime = movePointer( - start, end, steps, false, downTime, slowDown, gestureScope); + start, end, steps, false, downTime, downTime, slowDown, gestureScope); sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope); } - long movePointer(Point start, Point end, int steps, boolean isDecelerating, - long downTime, boolean slowDown, GestureScope gestureScope) { - long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, + long movePointer(Point start, Point end, int steps, boolean isDecelerating, long downTime, + long startTime, boolean slowDown, GestureScope gestureScope) { + long endTime = movePointer(downTime, startTime, steps * GESTURE_STEP_MS, isDecelerating, start, end, gestureScope); if (slowDown) { endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end, @@ -1445,35 +1484,45 @@ public final class LauncherInstrumentation { 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); } + private boolean hasTIS() { + return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(TestProtocol.REQUEST_HAS_TIS); + } + + public void sendPointer(long downTime, long currentTime, int action, Point point, GestureScope gestureScope) { - final boolean notLauncher3 = !isLauncher3(); + final boolean hasTIS = hasTIS(); switch (action) { case MotionEvent.ACTION_DOWN: if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER - && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) { + && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER + && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) { expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN); } - if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) { + if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) { expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS); } break; case MotionEvent.ACTION_UP: - if (notLauncher3 && gestureScope != GestureScope.INSIDE + if (hasTIS && gestureScope != GestureScope.INSIDE && gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) { expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS); } if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER - && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) { + && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER + && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) { expectEvent(TestProtocol.SEQUENCE_MAIN, gestureScope == GestureScope.INSIDE || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL); } - if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) { - expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS); + if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) { + expectEvent(TestProtocol.SEQUENCE_TIS, + gestureScope == GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE + || gestureScope == GestureScope.OUTSIDE_WITH_KEYCODE + ? EVENT_TOUCH_CANCEL_TIS : EVENT_TOUCH_UP_TIS); } break; } @@ -1503,11 +1552,11 @@ public final class LauncherInstrumentation { // vx0: initial speed at the x-dimension, set as twice the avg speed // dx: the constant deceleration at the x-dimension - double vx0 = 2 * (to.x - from.x) / duration; + double vx0 = 2.0 * (to.x - from.x) / duration; double dx = vx0 / duration; // vy0: initial speed at the y-dimension, set as twice the avg speed // dy: the constant deceleration at the y-dimension - double vy0 = 2 * (to.y - from.y) / duration; + double vy0 = 2.0 * (to.y - from.y) / duration; double dy = vy0 / duration; for (long i = 0; i < steps; ++i) { @@ -1596,9 +1645,10 @@ public final class LauncherInstrumentation { } Point getRealDisplaySize() { - final Point size = new Point(); - getContext().getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(size); - return size; + final Rect displayBounds = getContext().getSystemService(WindowManager.class) + .getMaximumWindowMetrics() + .getBounds(); + return new Point(displayBounds.width(), displayBounds.height()); } public void enableDebugTracing() { @@ -1642,6 +1692,29 @@ public final class LauncherInstrumentation { getTestInfo(TestProtocol.REQUEST_CLEAR_DATA); } + /** + * Reloads the workspace with a test layout that includes the Test Activity app icon on the + * hotseat. + */ + public void useTestWorkspaceLayoutOnReload() { + getTestInfo(TestProtocol.REQUEST_USE_TEST_WORKSPACE_LAYOUT); + } + + /** Reloads the workspace with the default layout defined by the user's grid size selection. */ + public void useDefaultWorkspaceLayoutOnReload() { + getTestInfo(TestProtocol.REQUEST_USE_DEFAULT_WORKSPACE_LAYOUT); + } + + /** Shows the taskbar if it is hidden, otherwise does nothing. */ + public void showTaskbarIfHidden() { + getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED); + } + + public List getHotseatIconNames() { + return getTestInfo(TestProtocol.REQUEST_HOTSEAT_ICON_NAMES) + .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + private String[] getActivities() { return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES) .getStringArray(TestProtocol.TEST_INFO_RESPONSE_FIELD); @@ -1758,4 +1831,4 @@ public final class LauncherInstrumentation { return ResourceUtils.getBoolByName( "config_supportsRoundedCornersOnWindows", resources, false); } -} \ No newline at end of file +} diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java deleted file mode 100644 index 42b6bc9b3a..0000000000 --- a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.tapl; - -import androidx.annotation.NonNull; -import androidx.test.uiautomator.By; -import androidx.test.uiautomator.UiObject2; -import androidx.test.uiautomator.Until; - -import com.android.launcher3.testing.TestProtocol; - -public class OptionsPopupMenuItem { - - private final LauncherInstrumentation mLauncher; - private final UiObject2 mObject; - - OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) { - mLauncher = launcher; - mObject = shortcut; - } - - /** - * Clicks the option. - */ - @NonNull - public void launch(@NonNull String expectedPackageName) { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - LauncherInstrumentation.log("OptionsPopupMenuItem before click " - + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject)); - mLauncher.clickLauncherObject(mObject); - mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START); - mLauncher.assertTrue( - "App didn't start: " + By.pkg(expectedPackageName), - mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)), - LauncherInstrumentation.WAIT_TIME_MS)); - } - } -} diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java index 0d06be373b..66a51a56a2 100644 --- a/tests/tapl/com/android/launcher3/tapl/Overview.java +++ b/tests/tapl/com/android/launcher3/tapl/Overview.java @@ -16,12 +16,7 @@ package com.android.launcher3.tapl; -import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL; - -import androidx.annotation.NonNull; - import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType; -import com.android.launcher3.testing.TestProtocol; /** * Overview pane. @@ -37,38 +32,6 @@ public final class Overview extends BaseOverview { return LauncherInstrumentation.ContainerType.OVERVIEW; } - /** - * Swipes up to All Apps. - * - * @return the App Apps object. - */ - @NonNull - public AllAppsFromOverview switchToAllApps() { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); - LauncherInstrumentation.Closable c = mLauncher.addContextLayer( - "want to switch from overview to all apps")) { - verifyActiveContainer(); - - // Swipe from an app icon to the top. - LauncherInstrumentation.log("Overview.switchToAllApps before swipe"); - mLauncher.swipeToState( - mLauncher.getDevice().getDisplayWidth() / 2, - mLauncher.getTestInfo( - TestProtocol.REQUEST_HOTSEAT_TOP). - getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD), - mLauncher.getDevice().getDisplayWidth() / 2, - 0, - 12, - ALL_APPS_STATE_ORDINAL, - LauncherInstrumentation.GestureScope.INSIDE); - - try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( - "swiped all way up from overview")) { - return new AllAppsFromOverview(mLauncher); - } - } - } - @Override public void dismissAllTasks() { super.dismissAllTasks(); diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java index c8c06e4da0..2f44bb6aa4 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java @@ -48,7 +48,7 @@ public class OverviewActions { try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "clicked screenshot button")) { UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject( - "global_screenshot_dismiss_image"); + "screenshot_dismiss_image"); if (mLauncher.getNavigationModel() != LauncherInstrumentation.NavigationModel.THREE_BUTTON) { mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index a860e7d894..c8caa42b6d 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -127,7 +127,7 @@ public final class OverviewTask { /** * Clicks at the task. */ - public Background open() { + public LaunchedAppState open() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { verifyActiveContainer(); mLauncher.executeAndWaitForEvent( @@ -137,7 +137,7 @@ public final class OverviewTask { + mTask.getParent().getContentDescription(), "clicking an overview task"); mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT); - return new Background(mLauncher); + return new LaunchedAppState(mLauncher); } } } diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java new file mode 100644 index 0000000000..82652c7f27 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import android.widget.TextView; + +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiObject2; + +/** + * Operations on search result page opened from home screen qsb. + */ +public class SearchResultFromQsb { + // The input resource id in the search box. + private static final String INPUT_RES = "input"; + private final LauncherInstrumentation mLauncher; + + SearchResultFromQsb(LauncherInstrumentation launcher) { + mLauncher = launcher; + mLauncher.waitForLauncherObject("search_container_all_apps"); + } + + /** Set the input to the search input edit text and update search results. */ + public void searchForInput(String input) { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to search for result with an input"); + LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + mLauncher.waitForLauncherObject(INPUT_RES).setText(input); + } + } + + /** Find the app from search results with app name. */ + public Launchable findAppIcon(String appName) { + UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName)); + return new AllAppsAppIcon(mLauncher, icon); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java new file mode 100644 index 0000000000..ce1c3c0e9d --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +/** Launchable that can serve as a source for dragging and dropping to splitscreen. */ +interface SplitscreenDragSource { + + /** + * Drags this app icon to the left (landscape) or bottom (portrait) of the screen, launching it + * in splitscreen. + * + * @param expectedNewPackageName package name of the app being dragged + * @param expectedExistingPackageName package name of the already-launched app + */ + default void dragToSplitscreen( + String expectedNewPackageName, String expectedExistingPackageName) { + Launchable launchable = getLaunchable(); + LauncherInstrumentation launcher = launchable.mLauncher; + try (LauncherInstrumentation.Closable e = launcher.eventsCheck()) { + LaunchedAppState.dragToSplitscreen( + launcher, launchable, expectedNewPackageName, expectedExistingPackageName); + } + } + + Launchable getLaunchable(); +} diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java new file mode 100644 index 0000000000..b5a08c3ba6 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import static com.android.launcher3.testing.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING; +import static com.android.launcher3.testing.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING; + +import android.graphics.Point; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.MotionEvent; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; +import androidx.test.uiautomator.UiObject2; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Operations on the Taskbar from LaunchedApp. + */ +public final class Taskbar { + + private final LauncherInstrumentation mLauncher; + + Taskbar(LauncherInstrumentation launcher) { + mLauncher = launcher; + } + + /** + * Returns an app icon with the given name. This fails if the icon is not found. + */ + @NonNull + public TaskbarAppIcon getAppIcon(String appName) { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to get a taskbar icon")) { + return new TaskbarAppIcon(mLauncher, mLauncher.waitForObjectInContainer( + mLauncher.waitForLauncherObject("taskbar_view"), + AppIcon.getAppIconSelector(appName, mLauncher))); + } + } + + /** + * Hides this taskbar. + * + * The taskbar must already be visible when calling this method. + */ + public void hide() { + mLauncher.getTestInfo(REQUEST_ENABLE_MANUAL_TASKBAR_STASHING); + + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to hide the taskbar"); + LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + mLauncher.waitForLauncherObject("taskbar_view"); + + final long downTime = SystemClock.uptimeMillis(); + Point stashTarget = new Point( + mLauncher.getRealDisplaySize().x - 1, mLauncher.getRealDisplaySize().y - 1); + + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, stashTarget, + LauncherInstrumentation.GestureScope.INSIDE); + LauncherInstrumentation.log("hideTaskbar: sent down"); + + try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) { + mLauncher.waitUntilLauncherObjectGone("taskbar_view"); + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, stashTarget, + LauncherInstrumentation.GestureScope.INSIDE); + } + } finally { + mLauncher.getTestInfo(REQUEST_DISABLE_MANUAL_TASKBAR_STASHING); + } + } + + /** + * Opens the Taskbar all apps page. + */ + public AllAppsFromTaskbar openAllApps() { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to open taskbar all apps"); + LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + + mLauncher.clickLauncherObject(mLauncher.waitForObjectInContainer( + mLauncher.waitForLauncherObject("taskbar_view"), getAllAppsButtonSelector())); + + return new AllAppsFromTaskbar(mLauncher); + } + } + + /** Returns a list of app icon names on the Taskbar */ + public List getIconNames() { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to get all taskbar icons")) { + return mLauncher.waitForObjectsInContainer( + mLauncher.waitForLauncherObject("taskbar_view"), + AppIcon.getAnyAppIconSelector()) + .stream() + .map(UiObject2::getText) + .filter(text -> !TextUtils.isEmpty(text)) // Filter out the all apps button + .collect(Collectors.toList()); + } + } + + private static BySelector getAllAppsButtonSelector() { + // Look for an icon with no text + return By.clazz(TextView.class).text(""); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java new file mode 100644 index 0000000000..099acd4716 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +import java.util.regex.Pattern; + +/** + * App icon specifically on the Taskbar. + */ +public final class TaskbarAppIcon extends AppIcon implements SplitscreenDragSource { + + private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onTaskbarItemLongClick"); + + TaskbarAppIcon(LauncherInstrumentation launcher, UiObject2 icon) { + super(launcher, icon); + } + + @Override + protected Pattern getLongClickEvent() { + return LONG_CLICK_EVENT; + } + + @Override + public TaskbarAppIconMenu openDeepShortcutMenu() { + return (TaskbarAppIconMenu) super.openDeepShortcutMenu(); + } + + @Override + protected TaskbarAppIconMenu createMenu(UiObject2 menu) { + return new TaskbarAppIconMenu(mLauncher, menu); + } + + @Override + public Launchable getLaunchable() { + return this; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenu.java new file mode 100644 index 0000000000..1f137c5406 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenu.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +/** + * Context menu of a Taskbar app icon. + */ +public final class TaskbarAppIconMenu extends AppIconMenu { + + TaskbarAppIconMenu(LauncherInstrumentation launcher, UiObject2 deepShortcutsContainer) { + super(launcher, deepShortcutsContainer); + } + + @Override + public TaskbarAppIconMenuItem getMenuItem(String shortcutText) { + return (TaskbarAppIconMenuItem) super.getMenuItem(shortcutText); + } + + @Override + protected TaskbarAppIconMenuItem createMenuItem(UiObject2 menuItem) { + return new TaskbarAppIconMenuItem(mLauncher, menuItem); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenuItem.java new file mode 100644 index 0000000000..69a8a08800 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIconMenuItem.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import androidx.test.uiautomator.UiObject2; + +import com.android.launcher3.testing.TestProtocol; + +import java.util.regex.Pattern; + +/** + * Menu item in a Taskbar app icon menu. + */ +public final class TaskbarAppIconMenuItem extends AppIconMenuItem implements SplitscreenDragSource { + + private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onTaskbarItemLongClick"); + + TaskbarAppIconMenuItem( + LauncherInstrumentation launcher, UiObject2 shortcut) { + super(launcher, shortcut); + } + + @Override + protected void addExpectedEventsForLongClick() { + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT); + } + + @Override + protected void waitForLongPressConfirmation() { + // On long-press, the popup container closes and the system drag-and-drop begins. This + // only leaves launcher views that were previously visible. + mLauncher.waitUntilLauncherObjectGone("popup_container"); + } + + @Override + protected String launchableType() { + return "taskbar app icon menu item"; + } + + @Override + public Launchable getLaunchable() { + return this; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java index f569ef4547..2346249564 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widget.java +++ b/tests/tapl/com/android/launcher3/tapl/Widget.java @@ -30,7 +30,7 @@ import java.util.regex.Pattern; /** * Widget in workspace or a widget list. */ -public final class Widget extends Launchable { +public final class Widget extends Launchable implements WorkspaceDragSource { private static final Pattern LONG_CLICK_EVENT = Pattern.compile("Widgets.onLongClick"); @@ -39,8 +39,8 @@ public final class Widget extends Launchable { } @Override - protected String getLongPressIndicator() { - return "drop_target_bar"; + protected void waitForLongPressConfirmation() { + mLauncher.waitForLauncherObject("drop_target_bar"); } @Override @@ -57,6 +57,12 @@ public final class Widget extends Launchable { return "widget"; } + /** This method requires public access, however should not be called in tests. */ + @Override + public Launchable getLaunchable() { + return this; + } + /** * Drags a non-configurable widget from the widgets container to the workspace and returns the * resize frame that is shown after the widget is added. diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 0bac2ca258..7fd68c09e9 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -115,6 +115,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText); final BySelector previewSelector = By.res(mLauncher.getLauncherPackageName(), "widget_preview"); + final int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen(); int i = 0; for (; ; ) { final Collection tableRows = mLauncher.getChildren(widgetsContainer); @@ -126,6 +127,9 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { if (label == null) { continue; } + if (widget.getVisibleCenter().y >= bottomGestureStartOnScreen) { + continue; + } mLauncher.assertEquals( "View is not WidgetCell", "com.android.launcher3.widget.WidgetCell", diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index 3f0d7fdc00..eb7f05bd1e 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -16,10 +16,12 @@ package com.android.launcher3.tapl; +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED; + import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL; import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; -import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL; +import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; import android.graphics.Point; @@ -31,12 +33,17 @@ import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.Direction; +import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; import com.android.launcher3.testing.TestProtocol; +import com.android.launcher3.testing.WorkspaceCellCenterRequest; import java.util.List; +import java.util.Map; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -49,6 +56,7 @@ public final class Workspace extends Home { private static final int DEFAULT_DRAG_STEPS = 10; private static final String DROP_BAR_RES_ID = "drop_target_bar"; private static final String DELETE_TARGET_TEXT_ID = "delete_target_text"; + private static final String UNINSTALL_TARGET_TEXT_ID = "uninstall_target_text"; static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile( "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W" @@ -56,7 +64,7 @@ public final class Workspace extends Home { static final Pattern EVENT_CTRL_W_UP = Pattern.compile( "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W" + ".*?metaState=META_CTRL_ON"); - private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick"); + static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick"); private final UiObject2 mHotseat; @@ -71,7 +79,7 @@ public final class Workspace extends Home { * @return the All Apps object. */ @NonNull - public AllApps switchToAllApps() { + public HomeAllApps switchToAllApps() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to switch from workspace to all apps")) { @@ -81,8 +89,8 @@ public final class Workspace extends Home { final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius()); final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1; final int swipeHeight = mLauncher.getTestInfo( - TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT). - getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); + TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); LauncherInstrumentation.log( "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY + ", swipeHeight = " + swipeHeight + ", slop = " @@ -98,11 +106,23 @@ public final class Workspace extends Home { try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "swiped to all apps")) { - return new AllApps(mLauncher); + return new HomeAllApps(mLauncher); } } } + /** + * Returns the home qsb. + * + * The qsb must already be visible when calling this method. + */ + public HomeQsb getQsb() { + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "want to get the home qsb")) { + return new HomeQsb(mLauncher); + } + } + /** * Returns an icon for the app, if currently visible. * @@ -110,13 +130,13 @@ public final class Workspace extends Home { * @return app icon, if found, null otherwise. */ @Nullable - public AppIcon tryGetWorkspaceAppIcon(String appName) { + public HomeAppIcon tryGetWorkspaceAppIcon(String appName) { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to get a workspace icon")) { final UiObject2 workspace = verifyActiveContainer(); final UiObject2 icon = workspace.findObject( AppIcon.getAppIconSelector(appName, mLauncher)); - return icon != null ? new AppIcon(mLauncher, icon) : null; + return icon != null ? new WorkspaceAppIcon(mLauncher, icon) : null; } } @@ -128,10 +148,10 @@ public final class Workspace extends Home { * @return app icon. */ @NonNull - public AppIcon getWorkspaceAppIcon(String appName) { + public HomeAppIcon getWorkspaceAppIcon(String appName) { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to get a workspace icon")) { - return new AppIcon(mLauncher, + return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer( verifyActiveContainer(), AppIcon.getAppIconSelector(appName, mLauncher))); @@ -165,33 +185,39 @@ public final class Workspace extends Home { } /** - * Drags an icon to the (currentPage + pageDelta) page if the page already exists. - * If the target page doesn't exist, the icon will be put onto an existing page that is the - * closest to the target page. + * Drags an icon to the (currentPage + pageDelta) page. + * If the target page doesn't exist yet, a new page will be created. + * In case the target page can't be created (e.g. existing pages are 0, 1, current: 0, + * pageDelta: 3, the latest page that can be created is 2) the icon will be dragged onto the + * page that can be created and is closest to the target page. * - * @param appIcon - icon to drag. - * @param pageDelta - how many pages should the icon be dragged from the current page. - * It can be a negative value. + * @param homeAppIcon - icon to drag. + * @param pageDelta - how many pages should the icon be dragged from the current page. + * It can be a negative value. currentPage + pageDelta should be greater + * than or equal to 0. */ - public void dragIcon(AppIcon appIcon, int pageDelta) { + public void dragIcon(HomeAppIcon homeAppIcon, int pageDelta) { + if (mHotseat.getVisibleBounds().height() > mHotseat.getVisibleBounds().width()) { + throw new UnsupportedOperationException( + "dragIcon does NOT support dragging when the hotseat is on the side."); + } try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { final UiObject2 workspace = verifyActiveContainer(); try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "dragging icon to page with delta: " + pageDelta)) { - dragIcon(workspace, appIcon, pageDelta); + dragIcon(workspace, homeAppIcon, pageDelta); verifyActiveContainer(); } } } - private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) { + private void dragIcon(UiObject2 workspace, HomeAppIcon homeAppIcon, int pageDelta) { int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen(); int targetX = (pageWidth / 2) + pageWidth * pageDelta; dragIconToWorkspace( mLauncher, - appIcon, + homeAppIcon, new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()), - "popup_container", false, false, () -> mLauncher.expectEvent( @@ -204,42 +230,52 @@ public final class Workspace extends Home { } @NonNull - public AppIcon getHotseatAppIcon(String appName) { - return new AppIcon(mLauncher, mLauncher.waitForObjectInContainer( + public HomeAppIcon getHotseatAppIcon(String appName) { + return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer( mHotseat, AppIcon.getAppIconSelector(appName, mLauncher))); } - private static int getStartDragThreshold(LauncherInstrumentation launcher) { - return launcher.getTestInfo(TestProtocol.REQUEST_START_DRAG_THRESHOLD).getInt( - TestProtocol.TEST_INFO_RESPONSE_FIELD); - } - - /* - * Get the center point of the delete icon in the drop target bar. + /** + * @return map of text -> center of the view. In case of icons with the same name, the one with + * lower x coordinate is selected. */ - private Point getDeleteDropPoint() { - return mLauncher.waitForObjectInContainer( - mLauncher.waitForLauncherObject(DROP_BAR_RES_ID), - DELETE_TARGET_TEXT_ID).getVisibleCenter(); + public Map getWorkspaceIconsPositions() { + final UiObject2 workspace = verifyActiveContainer(); + List workspaceIcons = + mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector()); + return workspaceIcons.stream() + .collect( + Collectors.toMap( + /* keyMapper= */ UiObject2::getText, + /* valueMapper= */ UiObject2::getVisibleCenter, + /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2)); + } + /* + * Get the center point of the delete/uninstall icon in the drop target bar. + */ + private static Point getDropPointFromDropTargetBar( + LauncherInstrumentation launcher, String targetId) { + return launcher.waitForObjectInContainer( + launcher.waitForLauncherObject(DROP_BAR_RES_ID), + targetId).getVisibleCenter(); } /** * Delete the appIcon from the workspace. * - * @param appIcon to be deleted. + * @param homeAppIcon to be deleted. * @return validated workspace after the existing appIcon being deleted. */ - public Workspace deleteAppIcon(AppIcon appIcon) { + public Workspace deleteAppIcon(HomeAppIcon homeAppIcon) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "removing app icon from workspace")) { dragIconToWorkspace( - mLauncher, appIcon, - () -> getDeleteDropPoint(), - true, /* decelerating */ - appIcon.getLongPressIndicator(), + mLauncher, + homeAppIcon, + () -> getDropPointFromDropTargetBar(mLauncher, DELETE_TARGET_TEXT_ID), () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), - null); + /* expectDropEvents= */ null); try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "dragged the app to the drop bar")) { @@ -248,6 +284,67 @@ public final class Workspace extends Home { } } + /** + * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar. + * + * @param launcher the root TAPL instrumentation object of {@link + * LauncherInstrumentation} type. + * @param homeAppIcon to be uninstalled. + * @param launcher the root TAPL instrumentation object of {@link + * LauncherInstrumentation} type. + * @param homeAppIcon to be uninstalled. + * @param expectLongClickEvents the runnable to be executed to verify expected longclick event. + * @return validated workspace after the existing appIcon being uninstalled. + */ + static Workspace uninstallAppIcon(LauncherInstrumentation launcher, HomeAppIcon homeAppIcon, + Runnable expectLongClickEvents) { + try (LauncherInstrumentation.Closable c = launcher.addContextLayer( + "uninstalling app icon")) { + dragIconToWorkspace( + launcher, + homeAppIcon, + () -> getDropPointFromDropTargetBar(launcher, UNINSTALL_TARGET_TEXT_ID), + expectLongClickEvents, + /* expectDropEvents= */null); + + launcher.waitUntilLauncherObjectGone(DROP_BAR_RES_ID); + + final BySelector installerAlert = By.text(Pattern.compile( + "Do you want to uninstall this app\\?", + Pattern.DOTALL | Pattern.MULTILINE)); + final UiDevice device = launcher.getDevice(); + assertTrue("uninstall alert is not shown", device.wait( + Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS)); + final UiObject2 ok = device.findObject(By.text("OK")); + assertNotNull("OK button is not shown", ok); + launcher.clickObject(ok); + assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait( + Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS)); + + try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer( + "uninstalled app by dragging to the drop bar")) { + return new Workspace(launcher); + } + } + } + + /** + * Get cell layout's grids size. The return point's x and y values are the cell counts in X and + * Y directions respectively, not the values in pixels. + */ + public Point getIconGridDimensions() { + int[] countXY = mLauncher.getTestInfo( + TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE).getIntArray( + TestProtocol.TEST_INFO_RESPONSE_FIELD); + return new Point(countXY[0], countXY[1]); + } + + static Point getCellCenter(LauncherInstrumentation launcher, int cellX, int cellY) { + return launcher.getTestInfo(WorkspaceCellCenterRequest.builder().setCellX( + cellX).setCellY(cellY).build()).getParcelable( + TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + /** * Finds folder icons in the current workspace. * @@ -259,31 +356,6 @@ public final class Workspace extends Home { o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList()); } - /** - * Drag an icon up with a short distance that makes workspace go to spring loaded state. - * - * @return the position after dragging. - */ - private static Point dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime, - UiObject2 icon, - String longPressIndicator, Runnable expectLongClickEvents) { - final Point iconCenter = icon.getVisibleCenter(); - final Point dragStartCenter = new Point(iconCenter.x, - iconCenter.y - getStartDragThreshold(launcher)); - - launcher.runToState(() -> { - launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, - iconCenter, LauncherInstrumentation.GestureScope.INSIDE); - LauncherInstrumentation.log("dragIconToSpringLoaded: sent down"); - expectLongClickEvents.run(); - launcher.waitForLauncherObject(longPressIndicator); - LauncherInstrumentation.log("dragIconToSpringLoaded: indicator"); - launcher.movePointer(iconCenter, dragStartCenter, DEFAULT_DRAG_STEPS, false, - downTime, true, LauncherInstrumentation.GestureScope.INSIDE); - }, SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start"); - return dragStartCenter; - } - private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, @Nullable Runnable expectedEvents) { launcher.runToState( @@ -300,36 +372,57 @@ public final class Workspace extends Home { } static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, - Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, + Point dest, boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents) { Runnable expectDropEvents = null; if (startsActivity || isWidgetShortcut) { expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START); } - dragIconToWorkspace(launcher, launchable, () -> dest, false, longPressIndicator, - expectLongClickEvents, expectDropEvents); + dragIconToWorkspace( + launcher, launchable, () -> dest, expectLongClickEvents, expectDropEvents); } /** - * Drag icon in workspace to else where. + * Drag icon in workspace to else where and drop it immediately. + * (There is no slow down time before drop event) * This function expects the launchable is inside the workspace and there is no drop event. */ - static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, - Supplier destSupplier, String longPressIndicator) { - dragIconToWorkspace(launcher, launchable, destSupplier, false, longPressIndicator, - () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), null); + static void dragIconToWorkspace( + LauncherInstrumentation launcher, Launchable launchable, Supplier destSupplier) { + dragIconToWorkspace( + launcher, + launchable, + destSupplier, + /* isDecelerating= */ false, + () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), + /* expectDropEvents= */ null); } static void dragIconToWorkspace( - LauncherInstrumentation launcher, Launchable launchable, Supplier dest, - boolean isDecelerating, String longPressIndicator, Runnable expectLongClickEvents, + LauncherInstrumentation launcher, + Launchable launchable, + Supplier dest, + Runnable expectLongClickEvents, + @Nullable Runnable expectDropEvents) { + dragIconToWorkspace(launcher, launchable, dest, /* isDecelerating */ true, + expectLongClickEvents, expectDropEvents); + } + + static void dragIconToWorkspace( + LauncherInstrumentation launcher, + Launchable launchable, + Supplier dest, + boolean isDecelerating, + Runnable expectLongClickEvents, @Nullable Runnable expectDropEvents) { try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( "want to drag icon to workspace")) { final long downTime = SystemClock.uptimeMillis(); - Point dragStart = dragIconToSpringLoaded(launcher, downTime, - launchable.getObject(), longPressIndicator, expectLongClickEvents); + Point dragStart = launchable.startDrag( + downTime, + expectLongClickEvents, + /* runToSpringLoadedState= */ true); Point targetDest = dest.get(); int displayX = launcher.getRealDisplaySize().x; @@ -338,9 +431,11 @@ public final class Workspace extends Home { while (targetDest.x > displayX || targetDest.x < 0) { int edgeX = targetDest.x > 0 ? displayX : 0; Point screenEdge = new Point(edgeX, targetDest.y); - launcher.movePointer(dragStart, screenEdge, DEFAULT_DRAG_STEPS, isDecelerating, - downTime, true, LauncherInstrumentation.GestureScope.INSIDE); - launcher.waitForIdle(); // Wait for the page change to happen + Point finalDragStart = dragStart; + executeAndWaitForPageScroll(launcher, + () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, + true, downTime, downTime, true, + LauncherInstrumentation.GestureScope.INSIDE)); targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1); dragStart = screenEdge; } @@ -348,11 +443,19 @@ public final class Workspace extends Home { // targetDest.x is now between 0 and displayX so we found the target page, // we just have to put move the icon to the destination and drop it launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating, - downTime, true, LauncherInstrumentation.GestureScope.INSIDE); + downTime, SystemClock.uptimeMillis(), false, + LauncherInstrumentation.GestureScope.INSIDE); dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents); } } + private static void executeAndWaitForPageScroll(LauncherInstrumentation launcher, + Runnable command) { + launcher.executeAndWaitForEvent(command, + event -> event.getEventType() == TYPE_VIEW_SCROLLED, + () -> "Page scroll didn't happen", "Scrolling page"); + } + /** * Flings to get to screens on the right. Waits for scrolling and a possible overscroll * recoil to complete. @@ -429,4 +532,4 @@ public final class Workspace extends Home { return widget != null ? new Widget(mLauncher, widget) : null; } } -} \ No newline at end of file +} diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java new file mode 100644 index 0000000000..114e6a586f --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import android.graphics.Point; + +import androidx.test.uiautomator.UiObject2; + +import java.util.regex.Pattern; + +/** + * App icon in workspace. + */ +final class WorkspaceAppIcon extends HomeAppIcon { + + WorkspaceAppIcon(LauncherInstrumentation launcher, UiObject2 icon) { + super(launcher, icon); + } + + @Override + protected Pattern getLongClickEvent() { + return Workspace.LONG_CLICK_EVENT; + } + + boolean isInCell(int cellX, int cellY) { + final Point center = Workspace.getCellCenter(mLauncher, cellX, cellY); + return mObject.getParent().getVisibleBounds().contains(center.x, center.y); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java new file mode 100644 index 0000000000..d8d4420005 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.tapl; + +import android.graphics.Point; + +/** Launchable that can serve as a source for dragging and dropping to the workspace. */ +interface WorkspaceDragSource { + + /** + * Drags an object to the center of homescreen. + * + * @param startsActivity whether it's expected to start an activity. + * @param isWidgetShortcut whether we drag a widget shortcut + */ + default void dragToWorkspace(boolean startsActivity, boolean isWidgetShortcut) { + Launchable launchable = getLaunchable(); + LauncherInstrumentation launcher = launchable.mLauncher; + try (LauncherInstrumentation.Closable e = launcher.eventsCheck()) { + final Point launchableCenter = launchable.getObject().getVisibleCenter(); + final Point displaySize = launcher.getRealDisplaySize(); + final int width = displaySize.x / 2; + Workspace.dragIconToWorkspace( + launcher, + launchable, + new Point( + launchableCenter.x >= width + ? launchableCenter.x - width / 2 + : launchableCenter.x + width / 2, + displaySize.y / 2), + startsActivity, + isWidgetShortcut, + launchable::addExpectedEventsForLongClick); + } + } + + /** This method requires public access, however should not be called in tests. */ + Launchable getLaunchable(); +}