diff --git a/Android.bp b/Android.bp index 19d2a5893b..78db013d3a 100644 --- a/Android.bp +++ b/Android.bp @@ -108,13 +108,11 @@ android_library { ], srcs: [ "tests/tapl/**/*.java", + "tests/tapl/**/*.kt", ], resource_dirs: [], manifest: "tests/tapl/AndroidManifest.xml", platform_apis: true, - lint: { - baseline_filename: "lint-baseline.xml", - }, } java_library_static { @@ -132,9 +130,6 @@ java_library_static { ], }, static_libs: ["libprotobuf-java-lite"], - lint: { - baseline_filename: "lint-baseline.xml", - }, } java_library_static { @@ -153,9 +148,6 @@ java_library_static { "libprotobuf-java-lite", "launcher_log_protos_lite", ], - lint: { - baseline_filename: "lint-baseline.xml", - }, } java_library { @@ -167,9 +159,6 @@ java_library { sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, - lint: { - baseline_filename: "lint-baseline.xml", - }, } // Library with all the dependencies for building Launcher3 @@ -187,6 +176,7 @@ android_library { "androidx.preference_preference", "androidx.slice_slice-view", "androidx.cardview_cardview", + "androidx.window_window", "com.google.android.material_material", "iconloader_base", "view_capture", @@ -196,7 +186,7 @@ android_library { sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, lint: { - baseline_filename: "lint-baseline-res-lib.xml", + baseline_filename: "lint-baseline2.xml", }, } @@ -213,12 +203,14 @@ android_library { "animationlib", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", + "android.appwidget.flags-aconfig-java", + "com.android.window.flags.window-aconfig-java", ], sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, manifest: "AndroidManifest-common.xml", lint: { - baseline_filename: "lint-baseline-common-deps-lib.xml", + baseline_filename: "lint-baseline2.xml", }, } @@ -265,7 +257,7 @@ android_app { "AndroidManifest-common.xml", ], lint: { - baseline_filename: "lint-baseline-launcher3.xml", + baseline_filename: "lint-baseline.xml", }, } @@ -289,9 +281,6 @@ android_library { ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", - lint: { - baseline_filename: "lint-baseline.xml", - }, } // Library with all the dependencies for building Launcher Go @@ -323,9 +312,8 @@ android_library { "AndroidManifest-common.xml", ], min_sdk_version: "current", - lint: { - baseline_filename: "lint-baseline-go-res-lib.xml", - }, + // TODO(b/319712088): re-enable use_resource_processor + use_resource_processor: false, } // Build rule for Quickstep library @@ -354,9 +342,8 @@ android_library { manifest: "quickstep/AndroidManifest.xml", platform_apis: true, min_sdk_version: "current", - lint: { - baseline_filename: "lint-baseline-launcher3.xml", - }, + // TODO(b/319712088): re-enable use_resource_processor + use_resource_processor: false, } // Build rule for Launcher3 Go app for Android Go devices. @@ -399,9 +386,6 @@ android_app { jacoco: { include_filter: ["com.android.launcher3.*"], }, - lint: { - baseline_filename: "lint-baseline.xml", - }, } @@ -438,9 +422,6 @@ android_app { jacoco: { include_filter: ["com.android.launcher3.*"], }, - lint: { - baseline_filename: "lint-baseline.xml", - }, } @@ -491,8 +472,5 @@ android_app { jacoco: { include_filter: ["com.android.launcher3.*"], }, - lint: { - baseline_filename: "lint-baseline.xml", - }, } diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 7e824ec15c..a31ee80460 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -184,5 +184,9 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4f580e0bd6..517bd6d70e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,7 +20,7 @@ - + + + @@ -73,7 +79,7 @@ android:clearTaskOnLaunch="true" android:stateNotNeeded="true" android:theme="@style/LauncherTheme" - android:screenOrientation="unspecified" + android:screenOrientation="behind" android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize" android:resizeableActivity="true" android:resumeWhilePausing="true" diff --git a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml index a07aeaadcc..8b4127afa3 100644 --- a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml @@ -16,7 +16,8 @@ android:width="84dp" android:height="208dp" android:viewportWidth="84" - android:viewportHeight="208"> + android:viewportHeight="208" + android:autoMirrored="true"> diff --git a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml index e20458e350..3a11f21873 100644 --- a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml @@ -16,7 +16,8 @@ android:width="122dp" android:height="303dp" android:viewportWidth="122" - android:viewportHeight="303"> + android:viewportHeight="303" + android:autoMirrored="true"> diff --git a/quickstep/res/drawable/button_taskbar_edu_colored.xml b/quickstep/res/drawable/button_taskbar_edu_colored.xml index a94a996499..104c4b2cc8 100644 --- a/quickstep/res/drawable/button_taskbar_edu_colored.xml +++ b/quickstep/res/drawable/button_taskbar_edu_colored.xml @@ -20,7 +20,7 @@ android:color="?android:attr/colorControlHighlight"> - + diff --git a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml index 938934073f..c217be2f26 100644 --- a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml @@ -16,7 +16,8 @@ android:width="83dp" android:height="208dp" android:viewportWidth="83" - android:viewportHeight="208"> + android:viewportHeight="208" + android:autoMirrored="true"> diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml index 77a82958ba..f246cbc30c 100644 --- a/quickstep/res/drawable/ic_chevron_down.xml +++ b/quickstep/res/drawable/ic_chevron_down.xml @@ -13,35 +13,22 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - - - - - - - - + + + + + diff --git a/quickstep/res/drawable/view_carousel.xml b/quickstep/res/drawable/view_carousel.xml new file mode 100644 index 0000000000..16c8e78aa3 --- /dev/null +++ b/quickstep/res/drawable/view_carousel.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml index 69e157433a..38df75659f 100644 --- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml +++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml @@ -36,19 +36,19 @@ app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toStartOf="@id/thumbnail_2"/> + app:layout_constraintTop_toTopOf="@id/thumbnail_1" + app:layout_constraintBottom_toBottomOf="@id/thumbnail_1" + app:layout_constraintStart_toStartOf="@id/thumbnail_1" + app:layout_constraintEnd_toEndOf="@id/thumbnail_1"/> + app:layout_constraintTop_toTopOf="@id/thumbnail_2" + app:layout_constraintBottom_toBottomOf="@id/thumbnail_2" + app:layout_constraintStart_toStartOf="@id/thumbnail_2" + app:layout_constraintEnd_toEndOf="@id/thumbnail_2"/> diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml index 672440f3e7..e4942aee42 100644 --- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml @@ -159,7 +159,7 @@ style="@style/TextAppearance.GestureTutorial.ButtonLabel" android:id="@+id/gesture_tutorial_menu_done_button" android:layout_width="wrap_content" - android:layout_height="40dp" + android:layout_height="48dp" android:layout_marginVertical="16dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml index c8ee6e9322..668a2e124a 100644 --- a/quickstep/res/layout/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml @@ -157,7 +157,7 @@ style="@style/TextAppearance.GestureTutorial.ButtonLabel" android:id="@+id/gesture_tutorial_menu_done_button" android:layout_width="wrap_content" - android:layout_height="40dp" + android:layout_height="48dp" android:layout_marginVertical="16dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml index b7acb70721..fb9bf998ec 100644 --- a/quickstep/res/layout/icon_app_chip_view.xml +++ b/quickstep/res/layout/icon_app_chip_view.xml @@ -17,45 +17,41 @@ + android:elevation="@dimen/task_thumbnail_icon_menu_elevation" + android:background="?androidprv:attr/materialColorSurfaceBright"> - - - + + + + + app:layout_constraintTop_toTopOf="@id/thumbnail_1" + app:layout_constraintBottom_toBottomOf="@id/thumbnail_1" + app:layout_constraintStart_toStartOf="@id/thumbnail_1" + app:layout_constraintEnd_toEndOf="@id/thumbnail_1"/> + app:layout_constraintTop_toTopOf="@id/thumbnail_2" + app:layout_constraintBottom_toBottomOf="@id/thumbnail_2" + app:layout_constraintStart_toStartOf="@id/thumbnail_2" + app:layout_constraintEnd_toEndOf="@id/thumbnail_2"/> diff --git a/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml b/quickstep/res/layout/keyboard_quick_switch_taskview_thumbnail.xml similarity index 100% rename from quickstep/res/layout/keyboard_quick_switch_thumbnail.xml rename to quickstep/res/layout/keyboard_quick_switch_taskview_thumbnail.xml diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml index 5af8d5165f..2bba7880fa 100644 --- a/quickstep/res/layout/keyboard_quick_switch_view.xml +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -42,8 +42,8 @@ android:layout_width="@dimen/keyboard_quick_switch_no_recent_items_icon_size" android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size" android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin" - android:src="@drawable/ic_empty_recents" - android:tint="?androidprv:attr/materialColorOnSurfaceInverse" + android:src="@drawable/view_carousel" + android:tint="?androidprv:attr/materialColorOnSurface" android:importantForAccessibility="no" app:layout_constraintVertical_chainStyle="packed" diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml index 0bbbfd5165..1115ff2b7c 100644 --- a/quickstep/res/layout/split_instructions_view.xml +++ b/quickstep/res/layout/split_instructions_view.xml @@ -24,8 +24,7 @@ android:paddingTop="@dimen/split_instructions_vertical_padding" android:paddingBottom="@dimen/split_instructions_vertical_padding" android:elevation="@dimen/split_instructions_elevation" - android:visibility="gone" - android:importantForAccessibility="yes"> + android:visibility="gone"> - + android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml index 72d7485d8f..736706a70d 100644 --- a/quickstep/res/layout/taskbar.xml +++ b/quickstep/res/layout/taskbar.xml @@ -35,7 +35,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - - + + + + + + + + + + + + diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml index 0890a4e6b8..6af7cf466c 100644 --- a/quickstep/res/layout/transient_taskbar.xml +++ b/quickstep/res/layout/transient_taskbar.xml @@ -52,7 +52,7 @@ android:elevation="@dimen/bubblebar_elevation" /> - - + "Maak seker dat jy van die rand heel regs of heel links af swiep" "Maak seker dat jy van die regter- of linkerrand af na die middel van die skerm toe swiep en laat los" "Jy het geleer hoe om van regs af te swiep om terug te gaan. Nou kan jy leer hoe om tussen apps te wissel." + "Jy het die \"gaan terug\"-gebaar voltooi. Nou kan jy leer hoe om tussen programme te wissel." "Jy het die Gaan Terug-gebaar voltooi" "Maak seker dat jy nie te naby aan die onderkant van die skerm swiep nie" "Gaan na Instellings om sensitiwiteit van teruggebaar te verander" @@ -92,9 +93,10 @@ "toestel" "Stelselnavigasie-instellings" "Deel" - "Skermkiekie" + "Skermskoot" "Verdeel" "Tik op ’n ander app om verdeelde skerm te gebruik" + "Kies ’n ander app as jy verdeelde skerm wil gebruik" "Kanselleer" "Verlaat verdeeldeskermkeuse" "Kies nog ’n app as jy verdeelde skerm wil gebruik" @@ -111,6 +113,8 @@ "Kry appvoorstelle op grond van jou roetine" "Langdruk op die verdeler om die Taakbalk vas te speld" "Doen meer met die Taakbalk" + "Wys altyd die Taakbalk" + "Raak en hou die verdeler in om altyd die Taakbalk onderaan jou skerm te wys" "Maak toe" "Klaar" "Tuis" @@ -124,7 +128,7 @@ "Taakbalk word gewys" "Taakbalk is versteek" "Navigasiebalk" - "Wys Taakbalk altyd" + "Wys altyd Taakbalk" "Verander navigasiemodus" "Taakbalkverdeler" "Skuif na links bo" diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml index a825e3e700..a5f5359803 100644 --- a/quickstep/res/values-am/strings.xml +++ b/quickstep/res/values-am/strings.xml @@ -49,6 +49,7 @@ "ከቀኝ ጥግ ወይም ከግራ ጥግ ጠርዝ ጀምሮ ማንሸራተትዎን ያረጋግጡ" "ከቀኝ ወይም ከግራ ጠርዝ ወደ ማያ ገጹ መሃል ማንሸራተትዎን እና መልቀቅዎን ያረጋግጡ" "ወደ ኋላ ለመመለስ ከቀኝ ጀምሮ እንዴት ማንሸራተት እንደሚችሉ አውቀዋል። ቀጥለው መተግበሪያዎችን እንዴት መቀየር እንደሚችሉ ይወቁ።" + "ወደኋላ የመመለስ ምልክትን አጠናቀዋል። ቀጥሎም መተግበሪያዎችን እንዴት መቀየር እንደሚችሉ ይወቁ።" "ወደኋላ የመመለስ ምልክትን አጠናቅቀዋል" "ከማያ ገጹ ታችኛው ክፍል ጋር በጣም ጠጋ ብለው አለማንሸራተትዎን ያረጋግጡ" "ከኋላ ስሜት ሰጭነት ደረጃ ለመለወጥ ወደ ቅንብሮች ይመለሱ" @@ -95,6 +96,7 @@ "ቅጽበታዊ ገፅ ዕይታ" "ክፈል" "የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ መታ ያድርጉ" + "የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ ይምረጡ" "ይቅር" "ከተከፈለ ማያ ገፅ ምርጫ ይውጡ" "የተከፈለ ማያ ገጽን ለመቀበል ሌላ መተግበሪያ ይምረጡ" @@ -111,6 +113,8 @@ "በዕለት ተዕለት ተግባርዎ መሠረት የመተግበሪያ አስተያየቶችን ያግኙ" "የተግባር አሞሌውን ፒን ለማድረግ በአከፋፋዩ ላይ በረጅሙ ይጫኑ" "በተግባር አሞሌው ተጨማሪ ነገር ያድርጉ" + "የተግባር አሞሌውን ሁልጊዜ አሳይ" + "በማያ ገጽዎ ግርጌ ላይ ያለውን የተግባር አሞሌ ሁልጊዜ ለማሳየት፣ መክፈያን ይንኩ እና ይያዙ" "ዝጋ" "ተጠናቅቋል" "መነሻ" diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml index 09f146c614..d25b211a4a 100644 --- a/quickstep/res/values-ar/strings.xml +++ b/quickstep/res/values-ar/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "تثبيت" "شكل مجاني" - "ليست هناك عناصر تم استخدامها مؤخرًا" + "ما مِن عناصر تم استخدامها مؤخرًا" "إعدادات استخدام التطبيق" "محو الكل" "التطبيقات المستخدمة مؤخرًا" @@ -49,6 +49,7 @@ "تأكَّد من التمرير سريعًا من أقصى الحافة اليسرى أو اليمنى." "تأكَّد من التمرير سريعًا من الحافة اليسرى أو اليمنى إلى وسط الشاشة ثم ارفع إصبعك." "لقد تعلمت كيفية التمرير سريعًا من اليسار للرجوع. تعرّف بعد ذلك على كيفية التبديل بين التطبيقات." + "لقد أكملت التدريب على إيماءة الرجوع. تعرّف بعد ذلك على كيفية التبديل بين التطبيقات." "لقد أكملت التدريب على إيماءة الرجوع." "تأكَّد من عدم التمرير سريعًا بالقرب من أسفل الشاشة." "لتغيير مستوى حساسية إيماءة الرجوع، انتقِل إلى \"الإعدادات\"" @@ -95,9 +96,10 @@ "لقطة شاشة" "تقسيم" "انقر على تطبيق آخر لاستخدام وضع تقسيم الشاشة." + "اختَر تطبيقًا آخر لاستخدام \"وضع تقسيم الشاشة\"" "إلغاء" "الخروج من وضع تقسيم الشاشة" - "اختَر تطبيقًا آخر لاستخدام وضع تقسيم الشاشة." + "اختَر تطبيقًا آخر لاستخدام \"وضع تقسيم الشاشة\"" "لا يسمح التطبيق أو لا تسمح مؤسستك بهذا الإجراء." "التطبيقات المصغّرة غير متوفّرة حاليًا، يرجى اختيار تطبيق آخر." "هل تريد تخطي الدليل التوجيهي للتنقّل؟" @@ -106,11 +108,13 @@ "التخطي" "تدوير الشاشة" "التعريف بشريط التطبيقات" - "اسحب تطبيقًا إلى جانب الشاشة لاستخدام تطبيقََين في آنٍ واحد." + "اسحب تطبيقًا إلى جانب الشاشة لاستخدام تطبيقََين في آنٍ واحد" "مرِّر ببطء للأعلى لإظهار شريط التطبيقات" - "احصل على اقتراحات التطبيقات بناءً على سلسلة إجراءاتك." + "احصل على اقتراحات التطبيقات بناءً على سلسلة إجراءاتك" "اضغط مع الاستمرار على المقسِّم لتثبيت \"شريط التطبيقات\"" "إنجاز المزيد باستخدام شريط التطبيقات" + "عرض \"شريط التطبيقات\" دائمًا" + "انقر مع الاستمرار على أداة تقسيم الشاشة لعرض \"شريط التطبيقات\" دائمًا في أسفل الشاشة." "إغلاق" "تم" "الرئيسية" diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml index ea95d2ca6f..4a05bff76f 100644 --- a/quickstep/res/values-as/strings.xml +++ b/quickstep/res/values-as/strings.xml @@ -49,6 +49,7 @@ "আপুনি সোঁ অথবা বাওঁ কাষৰ একেবাৰে সীমাৰ পৰা ছোৱাইপ কৰাটো নিশ্চিত কৰক" "আপুনি স্ক্ৰীনৰ সোঁ অথবা বাওঁ কাষৰ পৰা মধ্যভাগলৈকে ছোৱাইপ কৰি এৰি দিয়াটো নিশ্চিত কৰক" "সোঁফালৰ পৰা ছোৱাইপ কৰি কেনেকৈ উভতি যাব লাগে, সেইটো আপুনি জানিলে। ইয়াৰ পাছত, এপ্‌ কেনেকৈ সলনি কৰিব সেয়া জানক।" + "আপুনি উভতি যাওক নিৰ্দেশটো সম্পূৰ্ণ কৰিলে। ইয়াৰ পাছত, এপ্‌ কেনেকৈ সলনি কৰিব সেয়া জানক।" "আপুনি উভতি যাওক নিৰ্দেশটো সম্পূৰ্ণ কৰিলে" "আপুনি স্ক্ৰীনৰ তলৰ অংশৰ বেছি ওচৰলৈ ছোৱাইপ নকৰাটো নিশ্চিত কৰক" "উভতি যোৱাৰ নির্দেশটোৰ সংবেদনশীলতা সলনি কৰিবলৈ ছেটিঙলৈ যাওক" @@ -95,6 +96,7 @@ "স্ক্ৰীনশ্বট" "বিভাজন কৰক" "বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপত টিপক" + "বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপ্ বাছনি কৰক" "বাতিল কৰক" "বিভাজিত স্ক্ৰীনৰ বাছনিৰ পৰা বাহিৰ হওক" "বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপ্ বাছক" @@ -111,6 +113,8 @@ "আপোনাৰ ৰুটিনৰ ওপৰত আধাৰিত এপৰ পৰামৰ্শ পাওক" "টাস্কবাৰ পিন কৰিবলৈ বিভাজকত দীঘলীয়া সময় টিপি থাকক" "টাস্কবাৰৰ জৰিয়তে অধিক কাৰ্য সম্পাদন কৰক" + "টাস্কবাৰডাল সদায় দেখুৱাওক" + "আপোনাৰ স্ক্ৰীনৰ তলত সদায় টাস্কবাৰডাল দেখুৱাবলৈ বিভাজকডাল স্পৰ্শ কৰি ধৰি ৰাখক" "বন্ধ কৰক" "হ’ল" "গৃহপৃষ্ঠা" diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml index 510749c4cf..bb51b42ebf 100644 --- a/quickstep/res/values-az/strings.xml +++ b/quickstep/res/values-az/strings.xml @@ -49,6 +49,7 @@ "Ən sağ və ya sol kənardan sürüşdürün" "Sağ və ya sol kənardan ekranın ortasına sürüşdürüb, buraxın" "Geri qayıtmaq üçün sağdan sürüşdürmək qaydasını öyrəndiniz. Sonra tətbiqləri keçirməyi öyrənin." + "Geri getmə jestini tamamladınız. Sonra tətbiqləri keçirməyi öyrənin." "Geri qayıtma jestini tamamladınız" "Barmağınızı ekranın aşağı kənarına çox yaxınlaşdırmayın" "Geri qayıtma jestinin həssaslığını dəyişmək üçün Ayarlara keçin" @@ -95,6 +96,7 @@ "Skrinşot" "Ayırın" "Bölünmüş ekran üçün başqa tətbiqə toxunun" + "Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin" "Ləğv edin" "Bölünmüş ekran seçimindən çıxın" "Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin" @@ -111,6 +113,8 @@ "Rejiminizə əsasən tətbiq təklifləri alın" "Ayırıcı üzərinə basıb saxlayaraq İşləmə panelini bərkidin" "Tapşırıq paneli ilə daha çox şey edin" + "İşləmə panelini həmişə göstərin" + "İşləmə panelini həmişə ekranın aşağısında göstərmək üçün ayırıcı üzərinə toxunun və saxlayın" "Bağlayın" "Hazırdır" "Ev" diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml index 79e3616b19..5d7652b63d 100644 --- a/quickstep/res/values-b+sr+Latn/strings.xml +++ b/quickstep/res/values-b+sr+Latn/strings.xml @@ -49,6 +49,7 @@ "Obavezno prevucite od same desne ili leve ivice" "Obavezno prevucite od desne ili leve ivice do sredine ekrana i otpustite" "Naučili ste kako da prevlačite zdesna da biste se vratili unazad. Sada naučite da zamenite aplikacije." + "Dovršili ste pokret za povratak. Sada saznajte kako da promenite aplikacije." "Dovršili ste pokret za povratak" "Nikako ne prevlačite previše blizu dna ekrana" "Osetljivost pok. za nazad možete da promenite u Podešavanjima" @@ -95,6 +96,7 @@ "Snimak ekrana" "Podeli" "Dodirnite drugu aplikaciju za podeljeni ekran" + "Odaberite drugu aplikaciju da biste koristili podeljeni ekran" "Otkaži" "Izlazak iz biranja podeljenog ekrana" "Odaberite drugu aplikaciju za podeljeni ekran" @@ -107,10 +109,12 @@ "Rotirajte ekran" "Uputstva na traci zadataka" "Prevucite na stranu da biste koristili 2 aplikacije odjednom" - "Sporo prevucite nagore da biste prikazali traku zadataka" + "Sporo prevucite nagore da biste videli traku zadataka" "Dobijajte predloge aplikacija na osnovu rutine" - "Dugo pritiskajte razdelnik da biste zakačili traku zadataka" + "Dugo pritisnite razdelnik da biste zakačili traku zadataka" "Uradite više pomoću trake zadataka" + "Uvek prikazuj traku zadataka" + "Da bi traka zadataka uvek bila prikazana u dnu ekrana, dodirnite i zadržite razdelnik" "Zatvori" "Gotovo" "Početna" diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml index c164f95f46..646c6d9f49 100644 --- a/quickstep/res/values-be/strings.xml +++ b/quickstep/res/values-be/strings.xml @@ -49,6 +49,7 @@ "Правядзіце пальцам справа налева ці злева направа ад самага краю экрана" "Правядзіце пальцам ад правага або левага краю экрана ў цэнтр і адпусціце палец" "Вы даведаліся, як гартаць справа для вяртання. Цяпер даведайцеся, як пераключацца паміж праграмамі." + "Вы навучыліся рабіць жэст вяртання. А зараз даведайцеся, як пераключацца паміж праграмамі." "Вы навучыліся рабіць жэст для пераходу назад" "Не праводзьце пальцам занадта блізка да ніжняга краю экрана" "Каб змяніць адчувальнасць жэста вяртання, адкрыйце налады" @@ -95,6 +96,7 @@ "Здымак экрана" "Падзелены экран" "Каб падзяліць экран, націсніце на іншую праграму" + "Каб карыстацца рэжымам падзеленага экрана, выберыце другую праграму" "Скасаваць" "Выйсці з рэжыму падзеленага экрана" "Каб падзяліць экран, выберыце іншую праграму" @@ -111,6 +113,8 @@ "Атрымлівайце прапановы праграм з улікам вашых дзеянняў" "Замацуйце панэль задач доўгім націсканнем на раздзяляльнік" "Выкарыстоўвайце магчымасці панэлі задач" + "Замацуйце панэль задач унізе экрана" + "Для гэтага націсніце на раздзяляльнік і ўтрымлівайце яго" "Закрыць" "Гатова" "Галоўны экран" diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml index 923908911c..59de8defb7 100644 --- a/quickstep/res/values-bg/strings.xml +++ b/quickstep/res/values-bg/strings.xml @@ -49,6 +49,7 @@ "Трябва да плъзнете пръст от най-дясната или най-лявата част на екрана" "Трябва да плъзнете пръст от десния или левия край до средата на екрана, след което да го отпуснете" "Научихте жеста за връщане с плъзгане от дясно. Сега научете как се превключва между приложения." + "Изпълнихте жеста за връщане назад. В следващия урок ще научите как се превключва между приложения." "Изпълнихте жеста за връщане назад" "Не плъзвайте пръста си твърде близо до долната част на екрана" "Променете чувств. на жеста за връщане назад от настройките" @@ -95,6 +96,7 @@ "Екранна снимка" "Разделяне на екрана" "Докоснете друго прил., за да ползвате разд. екран" + "За разделен екран изберете още едно приложение" "Отказ" "Изход от избора на разделен екран" "За разделен екран изберете още едно приложение" @@ -111,6 +113,8 @@ "Получавайте предложения за приложения според навиците си" "Натиснете продължително разделителя, за да фиксирате лентата на задачите" "Правете повече неща с лентата на задачите" + "Лентата на задачите да се показва винаги" + "За да фиксирате лентата на задачите най-долу на екрана, докоснете и задръжте разделителя" "Затваряне" "Готово" "Начало" diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml index 80d0275956..28b1f86b10 100644 --- a/quickstep/res/values-bn/strings.xml +++ b/quickstep/res/values-bn/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "পিন করুন" "ফ্রি-ফর্ম" - "কোনো সাম্প্রতিক আইটেম নেই" + "কোনও সাম্প্রতিক আইটেম নেই" "অ্যাপ ব্যবহারের সেটিংস" "সবকিছু খালি করুন" "সম্প্রতি ব্যবহৃত অ্যাপ" @@ -49,6 +49,7 @@ "স্ক্রিনের একেবারে ডান বা বাঁদিকের প্রান্ত থেকে সোয়াইপ করেছেন কিনা তা দেখে নিন" "স্ক্রিনের ডান বা বাঁদিকের প্রান্ত থেকে মাঝখান পর্যন্ত সোয়াইপ করেছেন কিনা দেখে নিয়ে আঙুল তুলে নিন" "ফিরে যেতে, কীভাবে ডানদিক থেকে সোয়াইপ করতে হয় তা আপনি শিখেছেন। এরপর, একটি অ্যাপ থেকে অন্য অ্যাপে কীভাবে যাবেন জেনে নিন।" + "আপনি ফিরে যাওয়ার জেসচার সম্পর্কে জেনেছেন। এরপর, একটি অ্যাপ থেকে অন্য অ্যাপে কীভাবে যাবেন জেনে নিন।" "আপনি জেনেছেন হাতের জেসচার ব্যবহার করে আগের স্ক্রিনে কীভাবে ফিরে যাওয়া যায়" "স্ক্রিনের নিচের প্রান্তের খুব কাছে পর্যন্ত যাতে সোয়াইপ না করেন সেটি ভাল করে দেখে নিন" "ফিরে যাওয়ার জেসচারের সেন্সিটিভিটি পরিবর্তন করতে, সেটিংসে যান" @@ -95,6 +96,7 @@ "স্ক্রিনশট নিন" "স্প্লিট" "স্প্লিট স্ক্রিন ব্যবহারের জন্য অ্যাপে ট্যাপ করুন" + "স্প্লিট স্ক্রিন ব্যবহার করতে অন্য অ্যাপ বেছে নিন" "বাতিল করুন" "স্প্লিট স্ক্রিন বেছে নেওয়ার বিকল্প থেকে বেরিয়ে আসুন" "স্প্লিট স্ক্রিন ব্যবহার করতে অন্য অ্যাপ বেছে নিন" @@ -107,10 +109,12 @@ "স্ক্রিন ঘোরান" "টাস্কবার এডুকেশন" "একসাথে ২টি অ্যাপ ব্যবহার করতে একটি অ্যাপ পাশে টেনে আনুন" - "\'টাস্কবার\' দেখানোর জন্য উপরের দিকে আস্তে সোয়াইপ করুন" + "\'টাস্কবার\' দেখার জন্য উপরের দিকে ধীরে সোয়াইপ করুন" "আপনার রুটিন অনুযায়ী অ্যাপ থেকে সাজেশন পান" - "টাস্কবার পিন করতে, ড্রাইভার বেশ কিছুক্ষণ প্রেস করে রাখুন" + "\'টাস্কবার\' পিন করতে, ডিভাইডার বেশ কিছুক্ষণ প্রেস করে রাখুন" "\'টাস্কবার\' ফিচারের সাহায্যে আরও অনেক কিছু করুন" + "টাস্কবার সবসময় দেখানো" + "স্ক্রিনের নিচে টাস্কবার সবসময় দেখাতে ডিভাইডার টাচ করে ধরে থাকুন" "বন্ধ করুন" "হয়ে গেছে" "হোম" diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml index e22e8567f1..549091f885 100644 --- a/quickstep/res/values-bs/strings.xml +++ b/quickstep/res/values-bs/strings.xml @@ -49,6 +49,7 @@ "Prevucite s krajnjeg desnog ili krajnjeg lijevog ruba" "Prevucite s desnog ili lijevog ruba prema sredini ekrana i pustite" "Naučili ste kako prevući zdesna da se vratite. Sljedeće naučite kako prebacivati između aplikacija." + "Savladali ste pokret za vraćanje. Sljedeće naučite kako prebacivati između aplikacija." "Savladali ste pokret za vraćanje" "Pazite da ne prevučete preblizu donjem dijelu ekrana" "Promijenite osjetljivost pokreta za povratak u Postavkama" @@ -76,7 +77,7 @@ "Prevucite da prebacujete između aplikacija" "Da se prebacujete između aplikacija, prevucite s dna ekrana nagore, zadržite, a zatim pustite." "Da se prebacujete između aplikacija, prevucite s 2 prsta od dna ekrana, zadržite, a zatim pustite." - "Prebacujte se između aplikacija" + "Prebacujte između aplikacija" "Prevucite s dna ekrana prema gore, zadržite, a zatim pustite" "Odlično!" "Sve je spremno" @@ -95,6 +96,7 @@ "Snimak ekrana" "Podijeli" "Dodirnite drugu apl. da koristite podijeljeni ekran" + "Odaberite drugu aplikaciju da koristite podijeljeni ekran" "Otkaži" "Izlaz iz odabira podijeljenog ekrana" "Odaberite drugu apl. da koristite podijeljeni ekran" @@ -111,6 +113,8 @@ "Dobijajte prijedloge aplikacija zasnovane na vašoj rutini" "Pritisnite i zadržite razdjelnik da zakačite traku zadataka" "Uradite više pomoću trake zadataka" + "Stalni prikaz trake zadataka" + "Da se traka zadataka uvijek prikazuje na dnu ekrana, dodirnite i zadržite razdjelnik" "Zatvori" "Gotovo" "Dom" diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml index df8cb3679b..c16b64e909 100644 --- a/quickstep/res/values-ca/strings.xml +++ b/quickstep/res/values-ca/strings.xml @@ -49,6 +49,7 @@ "Assegura\'t de lliscar des de l\'extrem dret o esquerre de la pantalla." "Assegura\'t de lliscar des de la vora dreta o esquerra cap al centre de la pantalla i deixar anar" "Has après a lliscar des de la dreta per tornar enrere. Ara, descobreix com pots canviar d\'aplicació." + "Has completat el gest per tornar enrere. Ara, descobreix com pots canviar d\'aplicació." "Has completat el gest per tornar enrere" "Assegura\'t de no lliscar massa a prop de la part inferior de la pantalla." "Per canviar la sensibilitat del gest, ves a Configuració" @@ -95,6 +96,7 @@ "Captura de pantalla" "Divideix" "Toca una altra app per utilitzar pantalla dividida" + "Tria una altra aplicació per utilitzar la pantalla dividida" "Cancel·la" "Surt de la selecció de pantalla dividida" "Tria una altra app per utilitzar pantalla dividida" @@ -111,6 +113,8 @@ "Obtén suggeriments d\'aplicacions basats en la teva rutina" "Mantén premut el separador per fixar la Barra de tasques" "Treu més partit de la Barra de tasques" + "Mostra sempre la Barra de tasques" + "Perquè es mostri sempre la Barra de tasques a la part inferior de la pantalla, mantén premut el separador" "Tanca" "Fet" "Inici" @@ -124,7 +128,7 @@ "Es mostra la Barra de tasques" "S\'ha amagat la Barra de tasques" "Barra de navegació" - "Mostra sempre Barra de tasques" + "Barra de tasques sempre visible" "Canvia el mode de navegació" "Separador de la Barra de tasques" "Mou a la part superior o a l\'esquerra" diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml index 1cd2ed9162..1e58c9c087 100644 --- a/quickstep/res/values-cs/strings.xml +++ b/quickstep/res/values-cs/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Připnout" "Neomezený režim" - "Žádné nedávné položky" + "Žádné položky z nedávné doby" "Nastavení využití aplikací" "Vymazat vše" "Poslední aplikace" @@ -49,6 +49,7 @@ "Přejeďte prstem z úplného pravého nebo levého okraje obrazovky" "Přejeďte prstem z pravého nebo levého okraje doprostřed obrazovky a zdvihněte prst" "Naučili jste se, jak se vrátit zpět přejetím prstem zprava. Teď se naučíte přepínat mezi aplikacemi." + "Dokončili jste gesto pro přechod zpět. Teď se naučíte přepínat aplikace." "Dokončili jste gesto pro přechod zpět" "Dejte pozor, abyste prstem nepřejížděli moc blízko ke spodnímu okraji obrazovky" "Citlivost gesta pro přechod zpět můžete změnit v Nastavení" @@ -95,6 +96,7 @@ "Snímek obrazovky" "Rozdělit" "Obrazovku rozdělíte klepnutím na jinou aplikaci" + "Výběrem další aplikace rozdělíte obrazovku" "Zrušit" "Výběr opuštění rozdělené obrazovky" "Vyberte podporovanou aplikaci" @@ -109,8 +111,10 @@ "Přetáhněte aplikaci na stranu a používejte tak dvě najednou" "Panel aplikací zobrazíte pomalým přejetím prstem nahoru" "Dostávejte návrhy aplikací podle toho, jaké používáte" - "Dlouhým stisknutím oddělovače připnete panel aplikací" + "Dlouhým stisknutím oddělovače panel aplikací připnete" "Více možností s panelem aplikací" + "Stálé zobrazení panelu aplikací" + "Pokud chcete, aby se panel aplikací vždy zobrazoval ve spodní části obrazovky, podržte oddělovač." "Zavřít" "Hotovo" "Domů" diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml index d8f84dcdd9..cc534e759b 100644 --- a/quickstep/res/values-da/strings.xml +++ b/quickstep/res/values-da/strings.xml @@ -49,6 +49,7 @@ "Stryg fra kanten yderst til højre eller venstre" "Stryg fra højre eller venstre kant mod midten af skærmen, og løft fingeren" "Du har lært, hvordan du stryger fra højre for at gå tilbage. Nu skal du se, hvordan du skifter app." + "Du har fuldført bevægelsen for Gå tilbage. Som det næste kan du se, hvordan du skifter app." "Du har fuldført bevægelsen for Gå tilbage" "Undgå at stryge for tæt på bunden af skærmen" "Juster følsomheden for bevægelsen Gå tilbage i Indstillinger" @@ -95,6 +96,7 @@ "Screenshot" "Opdel" "Tryk på en anden app for at bruge opdelt skærm" + "Vælg en anden app for at bruge opdelt skærm" "Annuller" "Luk valg af opdelt skærm" "Vælg en anden app for at bruge opdelt skærm" @@ -111,6 +113,8 @@ "Få appforslag baseret på din rutine" "Fastgør proceslinjen med et langt tryk på skillelinjen" "Få mere fra hånden med proceslinjen" + "Vis altid proceslinjen" + "Hvis du vil have, at proceslinjen altid vises nederst på din skærm, skal du holde fingeren på skillelinjen" "Luk" "Luk" "Hjem" diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml index 3d4ce43607..f09e78cb54 100644 --- a/quickstep/res/values-de/strings.xml +++ b/quickstep/res/values-de/strings.xml @@ -49,6 +49,7 @@ "Wische vom äußersten rechten oder linken Displayrand" "Wische vom rechten oder linken Displayrand zur Displaymitte und lass los" "Du hast jetzt gelernt, vom rechten Displayrand aus zu wischen, um zurückzugehen. Gleich erfährst du, wie man zwischen Apps wechselt." + "Du hast die „Zurück“-Touch-Geste abgeschlossen. Gleich lernst du, wie man zwischen Apps wechselt." "Du hast den Schritt für die „Zurück“-Touch-Geste abgeschlossen" "Wische nicht zu nah an den unteren Displayrand" "Du kannst die Empfindlichkeit von „Zurück“ in den Einstellungen ändern" @@ -65,7 +66,7 @@ "Den Startbildschirm aufrufen" "Wenn du zum Startbildschirm gehen möchtest, wische einfach vom unteren Displayrand nach oben." "Wische mit zwei Fingern vom unteren Displayrand nach oben. So gelangst du immer zum Startbildschirm." - "Zum StartU+00AD­bildschirm" + "Zum Start­bildschirm" "Wische vom unteren Displayrand nach oben" "Gut gemacht!" "Wische vom unteren Displayrand nach oben" @@ -95,6 +96,7 @@ "Screenshot" "Teilen" "Für Splitscreen auf weitere App tippen" + "Für Splitscreen andere App auswählen" "Abbrechen" "Splitscreen-Auswahl beenden" "Für Splitscreen andere App auswählen" @@ -107,10 +109,12 @@ "Bildschirm drehen" "Informationen zur Taskleiste" "App zur Seite ziehen, um zwei Apps gleichzeitig zu nutzen" - "Langsam nach oben wischen, um die Taskleiste anzuzeigen" + "Langsam nach oben wischen, um die Taskleiste zu sehen" "App-Vorschläge auf Grundlage deiner Nutzung erhalten" "Bildschirmteiler lange drücken, um die Taskleiste anzupinnen" "Mehr Möglichkeiten mit der Taskleiste" + "Taskleiste immer anzeigen" + "Damit die Taskleiste immer unten angezeigt wird, halte den Teiler gedrückt" "Schließen" "Fertig" "Startbildschirm" @@ -129,7 +133,7 @@ "Taskleisten-Teiler" "Nach oben / Nach links verschieben" "Nach unten / Nach rechts verschieben" - "{count,plural, =1{# weitere App anzeigen.}other{# weitere Apps anzeigen.}}" + "{count,plural, =1{# weitere App anzeigen}other{# weitere Apps anzeigen}}" "%1$s und %2$s" "Hinzufügen einer App zum Desktop" "Abbrechen" diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml index 335ebacb7d..8c6b0f7503 100644 --- a/quickstep/res/values-el/strings.xml +++ b/quickstep/res/values-el/strings.xml @@ -49,6 +49,7 @@ "Φροντίστε να σύρετε από το άκρο της δεξιάς ή της αριστερής πλευράς." "Σύρετε από το δεξί ή το αριστερό άκρο προς το κέντρο της οθόνης και απομακρύνετε το δάχτυλό σας" "Μάθατε πώς να σύρετε από τα δεξιά για επιστροφή. Τώρα, μάθετε πώς να κάνετε εναλλαγή εφαρμογών." + "Ολοκληρώσατε την κίνηση επιστροφής. Στη συνέχεια, μάθετε πώς να κάνετε εναλλαγή εφαρμογών." "Ολοκληρώσατε την κίνηση επιστροφής" "Φροντίστε να μην σύρετε υπερβολικά κοντά στο κάτω μέρος της οθόνης" "Μεταβείτε στις Ρυθμίσεις για αλλαγή ευαισθ. κίνησης επιστρ." @@ -95,6 +96,7 @@ "Στιγμιότυπο οθόνης" "Διαχωρισμός" "Πατήστε άλλη εφαρμογή για διαχωρισμό οθόνης" + "Επιλέξτε άλλη εφαρμογή για διαχωρισμό οθόνης" "Ακύρωση" "Έξοδος από την επιλογή διαχωρισμού οθόνης" "Επιλέξτε άλλη εφαρμογή για διαχωρισμό οθόνης" @@ -111,6 +113,8 @@ "Λάβετε προτεινόμενες εφαρμογές με βάση τη ρουτίνα σας" "Παρατετ. πάτημα στο διαχωρ. για καρφ. της Γραμμής εργαλείων" "Κάντε περισσότερα με τη Γραμμή εργαλείων" + "Να εμφανίζεται πάντα η Γραμμή εργαλείων" + "Για να εμφανίζεται πάντα η Γραμμή εργαλείων στο κάτω μέρος της οθόνης, αγγίξτε παρατεταμένα το διαχωριστικό" "Κλείσιμο" "Τέλος" "Αρχική σελίδα" @@ -124,7 +128,7 @@ "Η γραμμή εργαλείων εμφανίζεται" "Η γραμμή εργαλείων είναι κρυφή" "Γραμμή πλοήγησης" - "Εμφ. πάντα σε Γραμμή εργαλείων" + "Εμφάνιση Γραμμής εργαλείων" "Αλλαγή τρόπου πλοήγησης" "Διαχωριστικό Γραμμής εργαλείων" "Μετακίνηση επάνω/αριστερά" diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml index 04ec03f608..5aa85ca48d 100644 --- a/quickstep/res/values-en-rAU/strings.xml +++ b/quickstep/res/values-en-rAU/strings.xml @@ -49,6 +49,7 @@ "Make sure you swipe from the far-right or far-left edge" "Make sure you swipe from the right or left edge to the middle of the screen and let go" "You\'ve learned how to swipe from the right to go back. Next, learn how to switch apps." + "You completed the go back gesture. Next, learn how to switch apps." "You completed the go back gesture" "Make sure you don\'t swipe too close to the bottom of the screen" "To change sensitivity of the back gesture, go to Settings" @@ -95,6 +96,7 @@ "Screenshot" "Split" "Tap another app to use split screen" + "Choose another app to use split screen" "Cancel" "Exit split screen selection" "Choose another app to use split screen" @@ -111,6 +113,8 @@ "Get app suggestions based on your routine" "Long press on the divider to pin the Taskbar" "Do more with the Taskbar" + "Always show the Taskbar" + "To always show the Taskbar on the bottom of your screen, touch and hold the divider" "Close" "Done" "Home" diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml index 5c8d0f2531..bdc3c22b2c 100644 --- a/quickstep/res/values-en-rCA/strings.xml +++ b/quickstep/res/values-en-rCA/strings.xml @@ -49,6 +49,7 @@ "Make sure you swipe from the far-right or far-left edge" "Make sure you swipe from the right or left edge to the middle of the screen and let go" "You learned how to swipe from the right to go back. Next up, learn how to switch apps." + "You completed the go back gesture. Next up, learn how to switch apps." "You completed the go back gesture" "Make sure you don\'t swipe too close to the bottom of the screen" "To change the sensitivity of the back gesture, go to Settings" @@ -95,6 +96,7 @@ "Screenshot" "Split" "Tap another app to use split screen" + "Choose another app to use split screen" "Cancel" "Exit split screen selection" "Choose another app to use split screen" @@ -111,6 +113,8 @@ "Get app suggestions based on your routine" "Long press on the divider to pin the Taskbar" "Do more with the Taskbar" + "Always show the Taskbar" + "To always show the Taskbar on the bottom of your screen, touch & hold the divider" "Close" "Done" "Home" diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml index 04ec03f608..5aa85ca48d 100644 --- a/quickstep/res/values-en-rGB/strings.xml +++ b/quickstep/res/values-en-rGB/strings.xml @@ -49,6 +49,7 @@ "Make sure you swipe from the far-right or far-left edge" "Make sure you swipe from the right or left edge to the middle of the screen and let go" "You\'ve learned how to swipe from the right to go back. Next, learn how to switch apps." + "You completed the go back gesture. Next, learn how to switch apps." "You completed the go back gesture" "Make sure you don\'t swipe too close to the bottom of the screen" "To change sensitivity of the back gesture, go to Settings" @@ -95,6 +96,7 @@ "Screenshot" "Split" "Tap another app to use split screen" + "Choose another app to use split screen" "Cancel" "Exit split screen selection" "Choose another app to use split screen" @@ -111,6 +113,8 @@ "Get app suggestions based on your routine" "Long press on the divider to pin the Taskbar" "Do more with the Taskbar" + "Always show the Taskbar" + "To always show the Taskbar on the bottom of your screen, touch and hold the divider" "Close" "Done" "Home" diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml index 04ec03f608..5aa85ca48d 100644 --- a/quickstep/res/values-en-rIN/strings.xml +++ b/quickstep/res/values-en-rIN/strings.xml @@ -49,6 +49,7 @@ "Make sure you swipe from the far-right or far-left edge" "Make sure you swipe from the right or left edge to the middle of the screen and let go" "You\'ve learned how to swipe from the right to go back. Next, learn how to switch apps." + "You completed the go back gesture. Next, learn how to switch apps." "You completed the go back gesture" "Make sure you don\'t swipe too close to the bottom of the screen" "To change sensitivity of the back gesture, go to Settings" @@ -95,6 +96,7 @@ "Screenshot" "Split" "Tap another app to use split screen" + "Choose another app to use split screen" "Cancel" "Exit split screen selection" "Choose another app to use split screen" @@ -111,6 +113,8 @@ "Get app suggestions based on your routine" "Long press on the divider to pin the Taskbar" "Do more with the Taskbar" + "Always show the Taskbar" + "To always show the Taskbar on the bottom of your screen, touch and hold the divider" "Close" "Done" "Home" diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml index 5d705ff9c8..4d87246cdc 100644 --- a/quickstep/res/values-en-rXC/strings.xml +++ b/quickstep/res/values-en-rXC/strings.xml @@ -49,6 +49,7 @@ "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‎‎‎‎‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‎‏‏‏‎‏‎‎‎‎‎‎‏‎Make sure you swipe from the far-right or far-left edge‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎‏‎‏‎‏‎‏‏‎‎‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‎‏‎‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‏‏‏‏‎‏‎‎Make sure you swipe from the right or left edge to the middle of the screen and let go‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‏‏‎‏‎‎‏‎‎‎‏‎‎‏‏‏‎‏‎‏‏‏‏‏‎‎‎‏‏‏‎You learned how to swipe from the right to go back. Next up, learn how to switch apps.‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‏‎‏‏‎‏‎‏‎‎‏‎‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎‎You completed the go back gesture. Next up, learn how to switch apps.‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‏‎‏‏‏‏‎‎‏‎‎‎‏‏‏‏‎‎‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‏‎‎‏‎‏‏‎‏‏‎‎You completed the go back gesture‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‏‏‏‏‎‎‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‎‏‏‏‎‏‏‏‏‎‏‏‎‎‎‎‏‎‎‎‎‎‏‎‏‎‎‏‎Make sure you don\'t swipe too close to the bottom of the screen‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‏‏‎‎‏‏‏‏‎‏‏‎‎‎‏‏‏‎‏‎‎‎‏‏‏‏‏‎‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‏‎‎‏‎‎‎‏‎‎To change the sensitivity of the back gesture, go to Settings‎‏‎‎‏‎" @@ -95,6 +96,7 @@ "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‏‎‏‏‎‏‎‏‎‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‎‏‎Screenshot‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‏‎‎‎Split‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎Tap another app to use split screen‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‎‎‎‎‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‏‎‏‎‏‏‎‎‏‎‎Choose another app to use split screen‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‏‎‎‏‏‎‏‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‏‏‏‎‎‏‎‎‏‏‎""‎‏‎‎‏‏‏‎Cancel‎‏‎‎‏‏‎""‎‏‎‎‏‏‏‎‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‎‎‏‎‏‎‎‎‏‏‎‎‏‎‎Exit split screen selection‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‎‏‏‏‎‎‎‎‏‎‎‏‎‎‏‎‏‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‎Choose another app to use split screen‎‏‎‎‏‎" @@ -111,6 +113,8 @@ "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‎‎‏‎‎‎‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‏‎‎Get app suggestions based on your routine‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎‏‎‎‏‎‏‎‎‏‎‎‎‎‏‏‎‎Long press on the divider to pin the Taskbar‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‎Do more with the Taskbar‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‎‏‎‎‏‏‎‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‎‎‎Always show the Taskbar‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‎‏‏‎‏‏‎‏‏‏‎‏‏‏‎‎‎‏‏‎To always show the Taskbar on the bottom of your screen, touch & hold the divider‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‎‎‎‏‎‎‏‏‏‏‎‏‎‏‎‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‏‏‎‎‏‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‎Close‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‏‎‎‎‎‏‏‎‎‎‎‏‎‎‏‎‏‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‎‏‏‏‏‎‎‏‎Done‎‏‎‎‏‎" "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‏‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‎‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎Home‎‏‎‎‏‎" diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml index a25bd7c550..66cf7b9429 100644 --- a/quickstep/res/values-es-rUS/strings.xml +++ b/quickstep/res/values-es-rUS/strings.xml @@ -49,6 +49,7 @@ "Asegúrate de deslizar desde el extremo derecho o izquierdo" "Recuerda deslizar desde el borde izquierdo o derecho hacia el centro de la pantalla y, luego, soltar" "Ya sabes deslizar el dedo desde la derecha para ir atrás. Ahora, descubre cómo cambiar de app." + "Completaste el gesto \"Atrás\". A continuación, obtén información para cambiar de app." "Completaste el gesto para ir atrás" "Asegúrate de no deslizar muy cerca de la parte inferior de la pantalla" "Cambia sensibilidad de gesto \"Atrás\" en Configuración" @@ -95,6 +96,7 @@ "Captura de pantalla" "Pantalla dividida" "Presiona otra app para usar la pantalla dividida" + "Elige otra app para usar la pantalla dividida" "Cancelar" "Salir de la selección de pantalla dividida" "Elige otra app para usar la pantalla dividida" @@ -106,11 +108,13 @@ "Omitir" "Girar pantalla" "Información sobre la barra de tareas" - "Arrastra una app a un lado para usar 2 apps a la vez" - "Desliza despacio hacia arriba para ver la Barra de tareas" - "Recibe sugerencias de aplicaciones basadas en tu rutina" + "Arrastra una app hacia un lado para usar 2 apps a la vez" + "Desliza lento hacia arriba para ver la Barra de tareas" + "Recibe sugerencias de apps basadas en tu rutina" "Mantén presionado el divisor para fijar la Barra de tareas" "Aprovecha mejor la Barra de tareas" + "Mostrar siempre la Barra de tareas" + "Mantén presionado el divisor para mostrar siempre la Barra de tareas en la parte inferior de la pantalla" "Cerrar" "Listo" "Botón de inicio" @@ -124,7 +128,7 @@ "Barra de tareas visible" "Barra de tareas oculta" "Barra de navegación" - "Ver siempre la Barra de tareas" + "Barra de tareas visible" "Cambiar el modo de navegación" "Divisor de la Barra de tareas" "Mover a la parte superior o izquierda" diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml index bb2f5335d2..0152b84b68 100644 --- a/quickstep/res/values-es/strings.xml +++ b/quickstep/res/values-es/strings.xml @@ -49,6 +49,7 @@ "Asegúrate de deslizar desde el borde derecho o izquierdo de la pantalla" "Asegúrate de deslizar desde el borde derecho o izquierdo de la pantalla hasta el centro y soltar" "Ya sabes deslizar el dedo desde la derecha para ir atrás. Descubre ahora cómo cambiar de aplicación." + "Has completado el gesto para volver. Ahora, descubre cómo cambiar de aplicación." "Has completado el gesto para volver" "No deslices demasiado cerca de la parte inferior de la pantalla" "Para cambiar la sensibilidad del gesto, ve a Ajustes" @@ -86,7 +87,7 @@ "¡Muy bien!" "Tutorial %1$d/%2$d" "¡Ya está!" - "Desliza el dedo hacia arriba para ir a la pantalla de inicio" + "Desliza hacia arriba para ir a la pantalla de inicio" "Toca el botón de inicio para ir a la pantalla de inicio" "Ya puedes empezar a usar tu %1$s" "dispositivo" @@ -95,9 +96,10 @@ "Hacer captura" "Dividir" "Toca otra aplicación para usar la pantalla dividida" + "Elige otra app para usar la pantalla dividida." "Cancelar" "Salir de la selección de pantalla dividida" - "Elige otra app para usar la pantalla dividida" + "Elige otra app para usar la pantalla dividida." "No puedes hacerlo porque la aplicación o tu organización no lo permiten" "Actualmente no se admiten widgets; selecciona otra aplicación" "¿Saltar tutorial de navegación?" @@ -109,8 +111,10 @@ "Arrastra una aplicación hacia un lado para usar 2 a la vez" "Desliza hacia arriba lentamente para ver la barra de tareas" "Obtén sugerencias de aplicaciones basadas en tu rutina" - "Mantén pulsado el divisor para fijar Barra de tareas" + "Mantén pulsado el divisor para fijar la barra de tareas" "Sácale más partido a la barra de tareas" + "Mostrar siempre la barra de tareas" + "Para mostrar siempre la barra de tareas en la parte inferior, mantén pulsada la línea divisoria" "Cerrar" "Hecho" "Inicio" @@ -124,7 +128,7 @@ "Barra de tareas visible" "Barra de tareas oculta" "Barra de navegación" - "Ver siempre Barra de Tareas" + "Barra de tareas visible" "Cambiar el modo de navegación" "Divisor de Barra de Tareas" "Mover arriba/a la izquierda" diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml index 93fbd1c171..fb4c8fa523 100644 --- a/quickstep/res/values-et/strings.xml +++ b/quickstep/res/values-et/strings.xml @@ -49,6 +49,7 @@ "Pühkige kindlasti parem- või vasakpoolsest servast" "Pühkige ekraani paremast või vasakust servast keskele ja eemaldage sõrm" "Õppisite, kuidas tagasiliikumiseks paremalt pühkida. Nüüd vaadake, kuidas rakenduste vahel vahetada." + "Tegite tagasiliikumise liigutuse. Järgmisena vaadake, kuidas rakenduste vahel vahetada." "Tegite tagasiliikumise liigutuse" "Veenduge, et te ei pühiks liiga ekraani allosa lähedalt." "Tagasiliigutuse tundlikkuse muutmiseks avage menüü Seaded" @@ -95,9 +96,10 @@ "Ekraanipilt" "Eralda" "Jagatud ekraanikuva kasutamiseks puudutage muud rakendust" + "Valige jagatud ekraanikuva jaoks muu rakendus." "Tühista" "Jagatud ekraanikuva valikust väljumine" - "Valige jagatud ekraanikuva jaoks muu rakendus" + "Valige jagatud ekraanikuva jaoks muu rakendus." "Rakendus või teie organisatsioon on selle toimingu keelanud" "Vidinaid praegu ei toetata, valige mõni muu rakendus" "Kas jätta navigeerimise õpetused vahele?" @@ -111,6 +113,8 @@ "Hankige oma rutiini põhjal rakenduste soovitusi" "Tegumiriba kinnitamiseks vajutage pikalt jagajat" "Tehke tegumiriba abil enamat" + "Alati kuvatud tegumiriba" + "Puudutage pikalt jaoturit, et tegumiriba oleks ekraani allosas alati kuvatud" "Sule" "Valmis" "Avaleht" @@ -124,7 +128,7 @@ "Tegumiriba on kuvatud" "Tegumiriba on peidetud" "Navigeerimisriba" - "Kuva tööriistariba alati" + "Kuva tegumiriba alati" "Navigeerimisrežiimi muutmine" "Tegumiriba jagaja" "Teisalda üles/vasakule" diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml index 53b3390a5c..4fe02c882b 100644 --- a/quickstep/res/values-eu/strings.xml +++ b/quickstep/res/values-eu/strings.xml @@ -31,10 +31,10 @@ "%1$s gelditzen dira gaur" "Aplikazioen iradokizunak" "Iradokitako aplikazioak" - "Jaso aplikazioen iradokizunak hasierako pantailaren beheko errenkadan" - "Jaso aplikazioen iradokizunak hasierako pantailako gogokoen errenkadan" - "Atzitu erraz aplikazio erabilienak hasierako pantailatik bertatik. Ohituren arabera aldatuko dira iradokizunak. Hasierako pantailara eramango dira beheko errenkadan dauden aplikazioak." - "Atzitu erraz aplikazio erabilienak hasierako pantailatik bertatik. Ohituren arabera aldatuko dira iradokizunak. Gogokoen errenkadako aplikazioak hasierako pantailara eramango ditugu." + "Jaso aplikazioen iradokizunak orri nagusiaren beheko errenkadan" + "Jaso aplikazioen iradokizunak orri nagusiko gogokoen errenkadan" + "Atzitu erraz aplikazio erabilienak orri nagusitik bertatik. Ohituren arabera aldatuko dira iradokizunak. Orri nagusira eramango dira beheko errenkadan dauden aplikazioak." + "Atzitu erraz aplikazio erabilienak orri nagusitik bertatik. Ohituren arabera aldatuko dira iradokizunak. Gogokoen errenkadako aplikazioak orri nagusira eramango ditugu." "Jaso aplikazioen iradokizunak" "Ez, eskerrik asko" "Ezarpenak" @@ -48,7 +48,8 @@ "Keinu bidezko nabigazioaren tutoriala osatzeko, biratu gailua" "Ziurtatu hatza pantailaren eskuineko edo ezkerreko ertzetik hasten zarela pasatzen" "Ziurtatu hatza pantailaren eskuineko edo ezkerreko ertzetik erdialdera pasatzen duzula eta ondoren hatza jasotzen duzula" - "Hatza eskuinetik pasatuta atzera egiten ikasi duzu. Jarraian, ikasi aplikazioa aldatzen." + "Hatza eskuinetik pasatuta atzera egiten ikasi duzu. Jarraian, lortu aplikazioz aldatzeko argibideak." + "Ikasi duzu atzera egiteko keinua. Jarraian, lortu aplikazioz aldatzeko argibideak." "Ikasi duzu atzera egiteko keinua" "Ziurtatu hatza ez duzula pasatzen pantailaren behealdetik gertuegi" "Keinuaren sentikortasuna aldatzeko, joan ezarpenetara" @@ -60,12 +61,12 @@ "Ziurtatu hatza pantailaren beheko ertzetik gora pasatzen duzula" "Ziurtatu ez duzula mugimendua gelditzen askatu arte" "Ziurtatu hatza zuzen pasatzen duzula gora" - "Ikasi duzu hasierako pantailara joateko keinua. Orain, ikasi atzera egiten." - "Ikasi duzu hasierako pantailara joateko keinua" - "Pasatu hatza hasierako pantailara joateko" - "Pasatu hatza pantailaren behealdetik gora. Keinu horrek hasierako pantailara eramango zaitu beti." - "Pasatu bi hatz pantailaren behealdetik gora. Hasierako pantailara eramango zaitu beti keinu horrek." - "Joan hasierako pantailara" + "Ikasi duzu orri nagusira joateko keinua. Orain, ikasi atzera egiten." + "Ikasi duzu orri nagusira joateko keinua" + "Pasatu hatza orri nagusira joateko" + "Pasatu hatza pantailaren behealdetik gora. Keinu horrek orri nagusira eramango zaitu beti." + "Pasatu bi hatz pantailaren behealdetik gora. Orri nagusira eramango zaitu beti keinu horrek." + "Joan orri nagusira" "Pasatu hatza pantailaren behealdetik gora" "Bikain!" "Ziurtatu hatza pantailaren beheko ertzetik gora pasatzen duzula" @@ -86,8 +87,8 @@ "Ederki!" "Tutoriala: %1$d/%2$d" "Dena prest!" - "Pasatu hatza gora hasierako pantailara joateko" - "Hasierako pantailara joateko, sakatu Hasiera botoia" + "Pasatu hatza gora orri nagusira joateko" + "Orri nagusira joateko, sakatu Hasiera botoia" "Prest zaude %1$s erabiltzen hasteko" "gailua" "Sisteman nabigatzeko ezarpenak" @@ -95,6 +96,7 @@ "Atera pantaila-argazki bat" "Zatitu" "Sakatu beste aplikazio bat pantaila zatitzeko" + "Pantaila zatitua erabiltzeko, aukeratu beste aplikazio bat" "Utzi" "Irten pantaila zatituaren hautapenetik" "Pantaila zatitzeko, aukeratu beste aplikazio bat" @@ -107,10 +109,12 @@ "Biratu pantaila" "Zereginen barra erabiltzeko argibideak" "Bi aplikazio batera erabiltzeko, arrastatu bat albo batera" - "Zereginen barra ikusteko, pasatu hatza gora poliki" + "Zereginen barra ikusteko, pasatu hatza gora mantso" "Jaso aplikazioen iradokizunak erabileran oinarrituta" "Zereginen barra ainguratzeko, sakatu zatitzailea luze" "Egin gauza gehiago zereginen barrarekin" + "Erakutsi beti zereginen barra" + "Pantailaren behealdeko zereginen barra beti erakusteko, eduki sakatuta zatitzailea" "Itxi" "Eginda" "Hasiera" diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml index c8da620e86..3324805e2a 100644 --- a/quickstep/res/values-fa/strings.xml +++ b/quickstep/res/values-fa/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "پین" "Freeform" - "بدون موارد اخیر" + "چیز جدیدی اینجا نیست" "تنظیمات استفاده از برنامه" "پاک کردن همه" "برنامه‌های اخیر" @@ -49,6 +49,7 @@ "دقت کنید که از انتهای لبه سمت راست یا سمت چپ تند بکشید" "دقت کنید که از لبه سمت راست یا سمت چپ تند به وسط صفحه بکشید و رها کنید" "یاد گرفتید چگونه برای رفتن به عقب از سمت راست تند بکشید. مورد بعدی، با نحوه جابه‌جا شدن بین برنامه‌ها آشنا شوید." + "اشاره برگشتن را تکمیل کردید. مورد بعدی، با نحوه جابه‌جا شدن بین برنامه‌ها آشنا شوید." "اشاره برگشتن را تکمیل کردید" "دقت کنید که موقع تند کشیدن، بیش‌از حد به پایین صفحه نزدیک نشوید" "برای تغییر حساسیت اشاره برگشت، به «تنظیمات» بروید" @@ -95,6 +96,7 @@ "نماگرفت" "دونیمه" "زدن روی برنامه‌ای دیگر برای استفاده از صفحه دونیمه" + "انتخاب برنامه‌ای دیگر برای استفاده از صفحه دونیمه" "لغو کردن" "خروج از انتخاب صفحهٔ دونیمه" "انتخاب برنامه‌ای دیگر برای استفاده از صفحه دونیمه" @@ -106,11 +108,13 @@ "رد شدن" "چرخاندن صفحه" "آموزش نوار وظیفه" - "برای استفاده هم‌زمان از ۲ برنامه، یک برنامه را به کناری بکشید" + "برای استفاده هم‌زمان از ۲ برنامه، یک برنامه را به‌کنار بکشید" "برای نمایش «نوار وظیفه»، انگشتتان را آهسته به‌بالا بکشید" "براساس روال‌هایتان، پیشنهاد برنامه دریافت کنید" - "برای سنجاق کردن «نوار وظیفه»، جداکننده را چند ثانیه فشار دهید" + "جداکننده را چند ثانیه فشار دهید تا «نوار وظیفه» سنجاق شود" "با «نوار وظیفه» می‌توانید کارهای بیشتر انجام دهید" + "همیشه نشان داده شدن «نوار وظیفه»" + "برای اینکه «نوار وظیفه» همیشه در پایین صفحه نشان داده شود، تقسیم‌کننده را لمس کنید و نگه دارید" "بستن" "تمام" "صفحه اصلی" diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml index c8c3490fe3..b92ef0522e 100644 --- a/quickstep/res/values-fi/strings.xml +++ b/quickstep/res/values-fi/strings.xml @@ -49,6 +49,7 @@ "Pyyhkäise aivan oikeasta tai vasemmasta reunasta" "Pyyhkäise näytön oikeasta tai vasemmasta reunasta keskelle ja päästä irti" "Osaat palata takaisin pyyhkäisemällä oikeasta reunasta. Opettele seuraavaksi vaihtamaan sovellusta." + "Olet oppinut Takaisin-eleen. Opettele seuraavaksi vaihtamaan sovellusta." "Olet oppinut takaisin-eleen" "Varo, ettet pyyhkäise liian lähellä alareunaa" "Voit muuttaa Takaisin-eleen herkkyyttä asetuksista" @@ -95,6 +96,7 @@ "Kuvakaappaus" "Jaa" "Avaa jaettu näyttö napauttamalla toista sovellusta" + "Käytä jaettua näyttöä valitsemalla toinen sovellus" "Peruuta" "Poistu jaetun näytön valinnasta" "Käytä jaettua näyttöä valitsemalla toinen sovellus" @@ -106,11 +108,13 @@ "Ohita" "Käännä näyttö" "Tehtäväpalkin ohje" - "Vedä sovellus sivuun, ja voit käyttää kahta sovellusta" + "Vedä sovellus sivuun ja käytä kahta sovellusta" "Näytä tehtäväpalkki pyyhkäisemällä ylös hitaasti" - "Sovellussuosituksia käytön perusteella" + "Vastaanota sovellussuosituksia käytön perusteella" "Kiinnitä tehtäväpalkki painamalla jakajaa pitkään" "Vinkkejä tehtäväpalkin tehokkaampaan käyttöön" + "Näytä tehtäväpalkki aina" + "Jos haluat tehtäväpalkin näkyvän aina näytön alaosassa, kosketa jakajaa pitkään" "Sulje" "Valmis" "Etusivu" diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml index 8a8a9aafbf..68ca935516 100644 --- a/quickstep/res/values-fr-rCA/strings.xml +++ b/quickstep/res/values-fr-rCA/strings.xml @@ -49,6 +49,7 @@ "Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche" "Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche vers le centre, puis allons-y" "Vous avez appris à balayer de la droite pour revenir en arrière. Apprenez comment changer d\'appli." + "Vous avez appris le geste de retour en arrière. Maintenant, apprenez comment changer d\'application." "Vous avez appris le geste de retour en arrière" "Assurez-vous de ne pas balayer trop près du bas de l\'écran" "Modifiez la sensibilité du geste de retour dans Paramètres" @@ -95,6 +96,7 @@ "Capture d\'écran" "Partager" "Toucher une autre appli pour partager l\'écran" + "Choisir une autre application pour utiliser l\'Écran divisé" "Annuler" "Quitter la sélection d\'écran divisé" "Choisir une autre application pour utiliser l\'écran partagé" @@ -106,11 +108,13 @@ "Ignorer" "Faire pivoter l\'écran" "Informations sur la barre des tâches" - "Pour utiliser deux applis, faites-les glisser vers le côté" - "Balayez lent. vers le haut pour afficher la barre des tâches" + "Faites glisser une appli sur le côté pour en utiliser deux à la fois" + "Balayez lentement vers le haut pour voir la barre des tâches" "Obtenez des suggestions d\'applis en fonction de vos routines" - "Maint. doigt sur séparateur pour épingler la barre de tâches" + "Maintenez le doigt sur le séparateur pour épingler la barre des tâches" "Faites-en plus avec la barre des tâches" + "Toujours afficher la Barre des tâches" + "Pour toujours afficher la Barre des tâches en bas de l\'écran, maintenez le doigt sur le séparateur" "Fermer" "OK" "Accueil" @@ -124,7 +128,7 @@ "Barre des tâches affichée" "Barre des tâches masquée" "Barre de navigation" - "Touj. afficher barre des tâches" + "Tjrs afficher barre des tâches" "Changer de mode de navigation" "Séparateur de la barre des tâches" "Déplacer vers le coin supérieur gauche de l\'écran" diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml index df6cad48dd..b554a48788 100644 --- a/quickstep/res/values-fr/strings.xml +++ b/quickstep/res/values-fr/strings.xml @@ -49,6 +49,7 @@ "Veillez à bien balayer l\'écran depuis le bord gauche ou droit" "Balayez bien l\'écran depuis le bord gauche ou droit jusqu\'au centre avant de relever le doigt" "Vous savez revenir en arrière en balayant depuis la droite. Apprenez à passer d\'une appli à l\'autre." + "Vous avez appris le geste pour revenir en arrière. Apprenez ensuite à passer d\'une appli à l\'autre." "Vous avez appris le geste pour revenir en arrière" "Veillez à ne pas balayer l\'écran trop près du bas" "Modifiez la sensibilité du geste retour dans les paramètres" @@ -95,6 +96,7 @@ "Capture d\'écran" "Partager" "Appuyez sur autre appli pour l\'écran partagé" + "Sélectionnez une autre appli pour utiliser l\'écran partagé." "Annuler" "Quitter la sélection de l\'écran partagé" "Sélect. autre appli pour utiliser l\'écran partagé" @@ -109,8 +111,10 @@ "Faites glisser une appli sur le côté pour en utiliser 2 à la fois" "Balayez lentement vers haut pour afficher barre des tâches" "Obtenez des suggestions d\'applis basées sur vos habitudes" - "Appui de manière prolongée sur le séparateur pour épingler la barre des tâches" + "Appui prolongé sur le séparateur pour épingler la barre des tâches" "Faites-en plus avec la barre des tâches" + "Toujours afficher la barre des tâches" + "Pour toujours afficher la barre des tâches en bas de votre écran, appuyez sur le séparateur de manière prolongée." "Fermer" "OK" "Accueil" @@ -124,7 +128,7 @@ "Barre des tâches affichée" "Barre des tâches masquée" "Barre de navigation" - "Toujours voir barre des tâches" + "Barre des tâches tjs visible" "Modifier le mode de navigation" "Séparateur de barre des tâches" "Déplacer en haut ou à gauche" diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml index 2022573a27..2207f17856 100644 --- a/quickstep/res/values-gl/strings.xml +++ b/quickstep/res/values-gl/strings.xml @@ -49,6 +49,7 @@ "Asegúrate de pasar o dedo desde o bordo dereito ou esquerdo" "Asegúrate de pasar o dedo desde o bordo dereito ou esquerdo ata o medio da pantalla e levantalo" "Aprendiches a pasar o dedo desde a dereita para volver. Agora, aprende a cambiar de aplicación." + "Completaches o xesto de volver á última pantalla. O próximo é aprender a cambiar de aplicación." "Completaches o xesto de volver á última pantalla" "Asegúrate de non pasar o dedo demasiado preto da parte inferior da pantalla" "Podes cambiar a sensibilidade do xesto en Configuración" @@ -95,6 +96,7 @@ "Facer captura" "Dividir" "Para usar a pantalla dividida, toca outra app" + "Escolle outra aplicación para usar a pantalla dividida." "Cancelar" "Saír da selección de pantalla dividida" "Escolle outra app para usar a pantalla dividida" @@ -109,8 +111,10 @@ "Arrastra unha aplicación cara a un lado para usar dúas á vez" "Pasa o dedo amodo cara arriba para ver a barra de tarefas" "Obtén suxestións de aplicacións en función da túa rutina" - "Mantén premida a liña divisoria para fixar a Barra de tarefas" + "Mantén premida a liña divisoria para fixar a barra de tarefas" "Tira máis proveito da barra de tarefas" + "Mostrar sempre a barra de tarefas" + "Para fixar a barra de tarefas na parte inferior, mantén premida a liña divisoria" "Pechar" "Listo" "Inicio" @@ -124,7 +128,7 @@ "Estase mostrando a barra de tarefas" "Non se está mostrando a barra de tarefas" "Barra de navegación" - "Manter Barra de tarefas" + "Ver sempre a barra de tarefas" "Cambiar modo de navegación" "Divisor da Barra de tarefas" "Mover á parte superior ou á esquerda" diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml index d1adf5c09b..f754675523 100644 --- a/quickstep/res/values-gu/strings.xml +++ b/quickstep/res/values-gu/strings.xml @@ -49,6 +49,7 @@ "ખાતરી કરો કે તમે એકદમ દૂરની જમણી કે ડાબી કિનારીએથી સ્વાઇપ કરો છો" "ખાતરી કરો કે તમે જમણી કે ડાબી કિનારીએથી સ્ક્રીનના મધ્ય ભાગ સુધી સ્વાઇપ કરો છો અને આંગળી ઊંચકી લો છો" "પાછળ જવા જમણેથી કેવી રીતે સ્વાઇપ કરવું એ તમે શીખી લીધું છે. હવે પછી, ઍપ સ્વિચ કરવાની રીત જાણો." + "તમે પાછા જવાનો સંકેત પૂર્ણ કર્યો છે. હવે પછી, ઍપ સ્વિચ કરવાની રીત વિશે જાણો." "તમે પાછા જવાનો સંકેત પૂર્ણ કર્યો છે" "ખાતરી કરો કે તમારાથી સ્ક્રીનની એકદમ નીચેની કિનારીની ખૂબ નજીક સુધી સ્વાઇપ ન થઈ જાય" "પાછા જવાના સંકેતની સંવેદિતા બદલવા માટે, સેટિંગમાં જાઓ" @@ -95,6 +96,7 @@ "સ્ક્રીનશૉટ" "વિભાજિત કરો" "વિભાજિત સ્ક્રીન વાપરવા, કોઈ અન્ય ઍપ પર ટૅપ કરો" + "વિભાજિત સ્ક્રીનની સુવિધાનો ઉપયોગ કરવા કોઈ અન્ય ઍપ પસંદ કરો" "રદ કરો" "\'સ્ક્રીનને વિભાજિત કરો\' પસંદગીમાંથી બહાર નીકળો" "સ્ક્રીન વિભાજનનો ઉપયોગ કરવા કોઈ અન્ય ઍપ પસંદ કરો" @@ -111,6 +113,8 @@ "તમારા રૂટિનના આધારે ઍપના સુઝાવો મેળવો" "ટાસ્કબારને પિન કરવા માટે, વિભાજકને થોડીવાર દબાવી રાખો" "ટાસ્કબાર વડે બીજું ઘણું કરો" + "ટાસ્કબાર હંમેશાં બતાવો" + "ટાસ્કબાર હંમેશાં તમારી સ્ક્રીનમાં સૌથી નીચે દેખાય તે માટે વિભાજકને ટચ કરીને થોડીવાર દબાવી રાખો" "બંધ કરો" "થઈ ગયું" "હોમ" diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml index 5ebe81418e..51351cb49f 100644 --- a/quickstep/res/values-hi/strings.xml +++ b/quickstep/res/values-hi/strings.xml @@ -21,9 +21,9 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "पिन करें" "फ़्रीफ़ॉर्म" - "हाल ही में इस्तेमाल किया गया कोई ऐप्लिकेशन नहीं है" + "हाल ही का कोई आइटम नहीं है" "ऐप्लिकेशन इस्तेमाल की सेटिंग" - "सभी ऐप्लिकेशन बंद करें" + "सभी हटाएं" "हाल ही में इस्तेमाल किए गए ऐप्लिकेशन" "टास्क बंद किया गया" "%1$s, %2$s" @@ -49,6 +49,7 @@ "स्क्रीन पर बिलकुल दाएं या बाएं किनारे से स्वाइप करें" "स्क्रीन पर दाएं या बाएं किनारे से बीच तक स्वाइप करें और फिर अपनी उंगली को स्क्रीन से हटा दें" "आपने स्क्रीन के दाएं किनारे से स्वाइप करके, पिछली स्क्रीन पर वापस जाने का तरीका सीख लिया है. अब, एक ऐप से दूसरे ऐप पर जाने का तरीका सीखें." + "आपने पीछे ले जाने वाले हाथ के जेस्चर के बारे में जान लिया है. एक ऐप से दूसरे पर जाने का तरीका जानें." "आपने जान लिया है कि हाथ का जेस्चर इस्तेमाल करके पिछली स्क्रीन पर वापस कैसे जाएं" "स्क्रीन पर बिलकुल नीचे तक स्वाइप न करें" "\'सेटिंग\' में जाकर, पीछे जाने के लिए इस्तेमाल होने वाले हाथ के जेस्चर (हाव-भाव) की संवेदनशीलता बदलें" @@ -95,6 +96,7 @@ "स्क्रीनशॉट लें" "स्प्लिट स्क्रीन मोड" "स्प्लिट स्क्रीन के लिए दूसरे ऐप्लिकेशन पर टैप करें" + "स्प्लिट स्क्रीन इस्तेमाल करने के लिए, दूसरा ऐप्लिकेशन चुनें" "अभी नहीं" "स्प्लिट स्क्रीन मोड से बाहर निकलें" "स्प्लिट स्क्रीन के लिए, दूसरा ऐप्लिकेशन चुनें" @@ -107,10 +109,12 @@ "स्क्रीन घुमाएं" "टास्कबार का ट्यूटोरियल" "किसी ऐप को किनारे की ओर ड्रैग करके 2 ऐप एक साथ इस्तेमाल करें" - "टास्कबार दिखाने के लिए, ऊपर की ओर धीरे से स्वाइप करें" + "टास्कबार देखने के लिए, ऊपर की ओर धीरे से स्वाइप करें" "डिवाइस के इस्तेमाल के आधार पर ऐप्लिकेशन के सुझाव पाएं" "टास्कबार को पिन करने के लिए डिवाइडर को दबाकर रखें" "टास्कबार की मदद से कई और काम करें" + "टास्कबार को हमेशा दिखाएं" + "टास्कबार को हमेशा अपनी स्क्रीन के नीचे दिखाने के लिए, डिवाइडर दबाकर रखें" "बंद करें" "हो गया" "होम" diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml index ad0be8e972..26ae082ae9 100644 --- a/quickstep/res/values-hr/strings.xml +++ b/quickstep/res/values-hr/strings.xml @@ -49,6 +49,7 @@ "Pazite da prijeđete prstom od krajnjeg desnog ili krajnjeg lijevog ruba" "Pazite da prijeđete prstom od desnog ili lijevog ruba do sredine zaslona i podignite prst" "Naučili ste kako prijeći prstom zdesna da biste se vratili. Sad saznajte kako promijeniti aplikaciju." + "Izvršili ste pokret za povratak. Sad saznajte kako promijeniti aplikaciju." "Izvršili ste pokret za povratak" "Pazite da ne prijeđete prstom preblizu dnu zaslona" "Osjetljivost pokreta povratka promijenite u postavkama" @@ -95,6 +96,7 @@ "Snimka zaslona" "Podijeli" "Dodirnite drugu aplikaciju za podijeljeni zaslon" + "Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona" "Odustani" "Zatvori odabir podijeljenog zaslona" "Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona" @@ -111,6 +113,8 @@ "Primajte prijedloge aplikacija na temelju svoje rutine" "Dugo pritisnite razdjelnik da biste prikvačili alatnu traku" "Učinite više pomoću trake sa zadacima" + "Uvijek prikazuj traku sa zadacima" + "Da bi se traka prikazivala, dodirnite i držite razdjelnik" "Zatvori" "Gotovo" "Početna" @@ -129,7 +133,7 @@ "Razdjelnik trake sa zadacima" "Premjesti gore/lijevo" "Premjesti dolje/desno" - "{count,plural, =1{Prikaži više aplikacija (još #).}one{Prikaži više aplikacija (još #).}few{Prikaži više aplikacija (još #).}other{Prikaži više aplikacija (još #).}}" + "{count,plural, =1{Prikaži još # aplikaciju}one{Prikaži još # aplikaciju}few{Prikaži još # aplikacije}other{Prikaži još # aplikacija}}" "%1$s i %2$s" "Dodavanje aplikacije na radnu površinu" "Odustani" diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml index b1298cebf8..7ea486d4d2 100644 --- a/quickstep/res/values-hu/strings.xml +++ b/quickstep/res/values-hu/strings.xml @@ -49,6 +49,7 @@ "Csúsztasson a képernyő jobb vagy bal széléről." "Csúsztassa ujját a képernyő jobb vagy bal széléről a képernyő közepéig, majd emelje fel." "Megtanulta, hogyan léphet vissza jobbról csúsztatva. A következő az appok közötti váltás." + "Teljesítette a visszalépési kézmozdulatot. Most megtanulhatja, hogyan válthat az appok között." "Teljesítette a visszalépési kézmozdulatot." "Ne csúsztasson túl közel a képernyő aljához." "A vissza mozdulat érzékenysége a Beállításokban módosítható" @@ -95,6 +96,7 @@ "Képernyőkép" "Felosztás" "Koppintson másik appra az osztott képernyőhöz" + "Válasszon másik appot a képernyő felosztásához" "Mégse" "Kilépés az osztott képernyő elemeinek kiválasztásából" "Válasszon másik appot a képernyő felosztásához" @@ -111,6 +113,8 @@ "Alkalmazásjavaslatokat kaphat a rutinja alapján" "A Feladatsáv kitűzéséhez nyomja meg hosszan az elválasztót" "Jobban kihasználhatja a Feladatsávot" + "Mindig jelenjen meg a Feladatsáv" + "Ahhoz, hogy a Feladatsáv mindig megjelenjen a képernyő alján, érintse meg és tartsa lenyomva az elválasztót" "Bezárás" "Kész" "Kezdőlap" @@ -124,7 +128,7 @@ "Feladatsáv megjelenítve" "Feladatsáv elrejtve" "Navigációs sáv" - "Mindig megjelenő feladatsáv" + "Mindig megjelenő Feladatsáv" "Navigációs mód módosítása" "Feladatsáv-elválasztó" "Mozgatás felülre vagy a bal oldalra" diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml index d896b510a7..367d378724 100644 --- a/quickstep/res/values-hy/strings.xml +++ b/quickstep/res/values-hy/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Ամրացնել" "Կամայական ձև" - "Վերջին տարրեր չկան" + "Այստեղ դեռ ոչինչ չկա" "Հավելվածի օգտագործման կարգավորումներ" "Փակել բոլորը" "Վերջին հավելվածներ" @@ -49,6 +49,7 @@ "Համոզվեք, որ մատը սահեցնում եք էկրանի աջ կամ ձախ եզրից" "Մատը սահեցրեք էկրանի աջ կամ ձախ եզրից դեպի կենտրոն և բաց թողեք" "Դուք սովորեցիք՝ ինչպես մատը աջից սահեցնելով հետ գնալ։ Այժմ սովորենք՝ ինչպես անցնել մի հավելվածից մյուսը։" + "Դուք սովորեցիք հետ գնալու ժեստը։ Այժմ սովորենք՝ ինչպես անցնել մի հավելվածից մյուսը։" "Դուք սովորեցիք հետ գնալու ժեստը" "Համոզվեք, որ մատը չափազանց մոտ չեք սահեցնում էկրանի ներքևի հատվածին" "Հետ գնալու ժեստի զգայունությունը փոփոխեք կարգավորումներում" @@ -95,9 +96,10 @@ "Սքրինշոթ անել" "Տրոհել" "Հպեք այլ հավելվածի՝ տրոհված էկրանից օգտվելու համար" + "Ընտրեք այլ հավելված՝ տրոհված էկրանից օգտվելու համար" "Չեղարկել" "Դուրս գալ տրոհված էկրանի ռեժիմից" - "Ընտրեք այլ հավելված՝ կիսված էկրանից օգտվելու համար" + "Ընտրեք այլ հավելված՝ տրոհված էկրանից օգտվելու համար" "Այս գործողությունն արգելված է հավելվածի կամ ձեր կազմակերպության կողմից" "Վիջեթները ներկայումս չեն աջակցվում. ընտրեք այլ հավելված" "Բաց թողնե՞լ նավիգացիայի ուղեցույցը" @@ -111,6 +113,8 @@ "Ստացեք առաջարկներ ձեր գործողությունների հիման վրա" "Հավելվածների վահանակն ամրացնելու համար երկար սեղմեք բաժանարարի վրա" "Օգտվեք հավելվածների վահանակի բոլոր հնարավորություններից" + "Ամրացրեք հավելվածների վահանակը" + "Հավելվածների վահանակն էկրանի ներքևում ամրացնելու համար հպեք և պահեք բաժանիչը" "Փակել" "Պատրաստ է" "Սկիզբ" @@ -124,7 +128,7 @@ "Խնդրագոտին ցուցադրվում է" "Խնդրագոտին թաքցված է" "Նավիգացիայի գոտի" - "Միշտ ցուցադրել հավելվածները" + "Միշտ ցույց տալ վահանակը" "Փոխել նավիգացիայի ռեժիմը" "Հավելվածների վահանակի բաժանիչ" "Տեղափոխել վերևի ձախ անկյուն" diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml index e6563ae24f..6208796516 100644 --- a/quickstep/res/values-in/strings.xml +++ b/quickstep/res/values-in/strings.xml @@ -49,6 +49,7 @@ "Pastikan Anda menggeser dari tepi ujung kanan atau ujung kiri" "Pastikan Anda menggeser dari tepi kanan atau kiri ke tengah layar, lalu lepaskan" "Anda telah belajar cara geser dari kanan untuk kembali. Berikutnya, pelajari cara beralih aplikasi." + "Anda telah menyelesaikan gestur kembali. Selanjutnya, pelajari cara beralih aplikasi." "Anda telah menyelesaikan gestur kembali" "Pastikan Anda tidak menggeser terlalu dekat ke bagian bawah layar" "Untuk mengubah sensitivitas gestur kembali, buka Setelan" @@ -95,9 +96,10 @@ "Screenshot" "Pisahkan" "Ketuk aplikasi lain untuk memakai layar terpisah" + "Pilih aplikasi lain untuk dibuka di layar terpisah" "Batal" "Keluar dari pemilihan layar terpisah" - "Pilih aplikasi lain untuk memakai layar terpisah" + "Pilih aplikasi lain untuk dibuka di layar terpisah" "Tindakan ini tidak diizinkan oleh aplikasi atau organisasi Anda" "Widget saat ini tidak didukung, pilih aplikasi lain" "Lewati tutorial gestur?" @@ -110,7 +112,9 @@ "Geser perlahan ke atas untuk menampilkan Taskbar" "Dapatkan saran aplikasi berdasarkan rutinitas Anda" "Tekan lama pemisah untuk menyematkan Taskbar" - "Lakukan lebih banyak dengan Taskbar" + "Lakukan lebih banyak hal dengan Taskbar" + "Selalu tampilkan Taskbar" + "Untuk selalu menampilkan Taskbar di bagian bawah layar Anda, sentuh & tahan pembatasnya" "Tutup" "Selesai" "Layar utama" @@ -129,7 +133,7 @@ "Pemisah Taskbar" "Pindahkan ke atas/kiri" "Pindahkan ke bawah/kanan" - "{count,plural, =1{Tampilkan # aplikasi lain.}other{Tampilkan # aplikasi lain.}}" + "{count,plural, =1{Tampilkan # aplikasi lainnya.}other{Tampilkan # aplikasi lainnya.}}" "%1$s dan %2$s" "Menambahkan aplikasi ke Desktop" "Batalkan" diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml index c297a6b46d..09249bc1dd 100644 --- a/quickstep/res/values-is/strings.xml +++ b/quickstep/res/values-is/strings.xml @@ -49,6 +49,7 @@ "Passaðu að strjúka frá jaðri hægri eða vinstri brúnar" "Passaðu að strjúka frá jaðri hægri eða vinstri brúnar að miðju skjásins og sleppa síðan" "Þú lærðir að strjúka frá hægri til að bakka. Næst skaltu læra hvernig þú skiptir á milli forrita." + "Þú laukst við að kynna þér bendinguna „til baka“. Næst skaltu læra hvernig þú skiptir á milli forrita." "Þú laukst við að kynna þér bendinguna „til baka“" "Passaðu að strjúka ekki of nálægt neðri brún skjásins" "Til að breyta næmi til baka-bendingar ferðu í stillingar" @@ -95,6 +96,7 @@ "Skjámynd" "Skipta" "Ýttu á annað forrit til að nota skjáskiptingu" + "Veldu annað forrit til að nota skjáskiptingu" "Hætta við" "Loka skjáskiptingu" "Veldu annað forrit til að nota skjáskiptingu" @@ -111,6 +113,8 @@ "Fáðu forritatillögur sem byggjast á rútínunni þinni" "Haltu skiptingu forritastikunnar inni til að festa hana" "Nýttu forritastikuna betur" + "Halda forritastikunni sýnilegri" + "Haltu skjáskiptingunni neðst á skjánum inni til að halda forritastikunni sýnilegri" "Loka" "Lokið" "Heim" diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml index c028d2606d..961a2ff7d6 100644 --- a/quickstep/res/values-it/strings.xml +++ b/quickstep/res/values-it/strings.xml @@ -49,6 +49,7 @@ "Assicurati di scorrere dal bordo all\'estrema destra o all\'estrema sinistra" "Assicurati di scorrere dal bordo destro o sinistro verso il centro dello schermo e solleva il dito" "Hai imparato a scorrere da destra per tornare indietro. Ora impara come passare da un\'app all\'altra." + "Hai completato il gesto Indietro. Ora, impara come passare da un\'app all\'altra." "Hai completato il gesto Indietro" "Assicurati di non scorrere troppo vicino alla parte inferiore dello schermo" "Usa Impostazioni per cambiare sensibilità del gesto Indietro" @@ -95,6 +96,7 @@ "Screenshot" "Dividi" "Tocca un\'altra app per usare lo schermo diviso" + "Scegli un\'altra app per usare lo schermo diviso" "Annulla" "Esci dalla selezione dello schermo diviso" "Scegli un\'altra app per usare lo schermo diviso" @@ -111,6 +113,8 @@ "Visualizza le app suggerite in base alla tua routine" "Premi a lungo sul divisore per fissare la barra delle app" "Fai di più con la barra delle app" + "Mostra sempre la barra delle app" + "Per mostrare sempre la barra delle app in basso, tocca e tieni premuto il divisore" "Chiudi" "Fine" "Home" @@ -124,7 +128,7 @@ "Barra delle app visualizzata" "Barra delle app nascosta" "Barra di navigazione" - "Mostra sempre barra delle app" + "Mostra sempre barra app" "Cambia modalità di navigazione" "Divisore barra delle app" "Sposta in alto/a sinistra" diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml index 505096bcc4..f84db183f2 100644 --- a/quickstep/res/values-iw/strings.xml +++ b/quickstep/res/values-iw/strings.xml @@ -49,6 +49,7 @@ "חשוב להחליק מהקצה השמאלי או הימני" "חשוב להחליק מהקצה השמאלי או הימני למרכז המסך ואז לשחרר" "למדת איך להחליק מצד ימין כדי לחזור אחורה. בשלב הבא לומדים איך לעבור בין אפליקציות." + "השלמת את תנועת \'הקודם\'. בשלב הבא לומדים איך לעבור בין אפליקציות." "השלמת את התנועה \'חזרה אחורה\'" "חשוב שלא להחליק קרוב מדי לתחתית המסך" "כדי לשנות את מידת הרגישות של תנועת החזרה, יש לעבור להגדרות" @@ -95,6 +96,7 @@ "צילום מסך" "פיצול" "צריך להקיש על אפליקציה אחרת כדי להשתמש במסך מפוצל" + "כדי להשתמש במסך מפוצל צריך לבחור אפליקציה אחרת" "ביטול" "יציאה מתצוגת מסך מפוצל" "כדי להשתמש במסך מפוצל צריך לבחור אפליקציה אחרת" @@ -107,10 +109,12 @@ "סיבוב המסך" "הסבר על סרגל האפליקציות" "כדי להשתמש בשתי אפליקציות בו-זמנית, צריך לגרור אפליקציה לצד" - "צריך להחליק לאט כדי להציג את סרגל האפליקציות" - "קבלת הצעות לאפליקציות על סמך השימוש השגרתי שלך" + "צריך להחליק לאט למעלה כדי להציג את סרגל האפליקציות" + "אפשר לקבל הצעות לאפליקציות על סמך השימוש השגרתי שלך" "כדי להצמיד את סרגל האפליקציות, לוחצים לחיצה ארוכה על המחיצה" "פעולות נוספות שאפשר לעשות עם סרגל האפליקציות" + "תמיד להציג את סרגל האפליקציות" + "כדי להציג תמיד את סרגל האפליקציות בתחתית המסך, יש ללחוץ לחיצה ארוכה על המחיצה" "סגירה" "סיום" "בית" diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml index e9f167087e..6b555c13a7 100644 --- a/quickstep/res/values-ja/strings.xml +++ b/quickstep/res/values-ja/strings.xml @@ -49,6 +49,7 @@ "右端または左端からスワイプしてください" "画面の右端または左端から中央に向かってスワイプし、指を離してください" "右側からスワイプして前の画面に戻る方法を学習しました。次は、アプリを切り替える方法を覚えましょう。" + "「戻る」操作を完了しました。次は、アプリを切り替える方法を覚えましょう。" "「戻る」操作を学習しました" "スワイプする際は画面の下部に近づきすぎないようにしましょう" "「戻る」操作の感度を変更するには [設定] に移動します" @@ -95,6 +96,7 @@ "スクリーンショット" "分割" "分割画面を使用するには、他のアプリをタップします" + "分割画面を使用するには別のアプリを選択してください" "キャンセル" "分割画面の選択を終了します" "分割画面にするには、別のアプリを選択してください" @@ -106,11 +108,13 @@ "スキップ" "画面を回転" "タスクバーの説明" - "アプリを横にドラッグして 2 個のアプリを同時に使用できます" + "アプリを横にドラッグすると 2 個のアプリを同時に使用できます" "タスクバーを表示するには、上にゆっくりとスワイプします" "毎日の使用状況に基づいてアプリの候補が表示されます" - "タスクバーを固定するには分割線を長押ししてください" + "分割線を長押ししてタスクバーを固定します" "タスクバーの各種機能" + "タスクバーを常に表示" + "タスクバーを画面下部に常に表示するには分割線を長押しします" "閉じる" "完了" "ホーム" @@ -124,7 +128,7 @@ "タスクバー表示" "タスクバー非表示" "ナビゲーション バー" - "常にタスクバーを表示" + "常にタスクバーを表示する" "ナビゲーション モードを変更" "タスクバーの区切り" "上 / 左に移動" diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml index 5bcf85632a..13d24459e9 100644 --- a/quickstep/res/values-ka/strings.xml +++ b/quickstep/res/values-ka/strings.xml @@ -49,6 +49,7 @@ "გადაფურცლეთ უკიდურესი მარჯვენა ან მარცხენა ბოლოდან" "გადაფურცლეთ მარჯვენა ან მარცხენა კიდიდან ეკრანის ცენტრისკენ და თითი აუშვით" "თქვენ ისწავლეთ მარჯვნიდან გადაფურცვლა უკან დასაბრუნებლად. ახლა კი შეიტყვეთ, როგორ გადართოთ აპები." + "თქვენ შეასრულეთ უკან დაბრუნების ჟესტი. ახლა კი შევიტყოთ, როგორ გადავრთოთ აპები." "თქვენ შეასრულეთ უკან დაბრუნების ჟესტი" "არ გადაფურცლოთ ეკრანის ბოლოსთან ახლოს" "დაბრუნების ჟესტის მგრძნობელობის შესაცვლელად გადადით პარამეტრებზე" @@ -95,6 +96,7 @@ "ეკრანის ანაბეჭდი" "გაყოფა" "შეეხეთ სხვა აპს ეკრანის გასაყოფად" + "აირჩიეთ სხვა აპი ეკრანის გასაყოფად" "გაუქმება" "ეკრანის გაყოფის არჩევანიდან გასვლა" "აირჩიეთ სხვა აპი ეკრანის გასაყოფად" @@ -111,6 +113,8 @@ "მიიღეთ აპის შეთავაზებები თქვენი რუტინის მიხედვით" "ხანგრძლივად დააჭირეთ გამყოფს ამოცანათა ზოლის ჩასამაგრებლად" "გააკეთეთ მეტი ამოცანათა ზოლის მეშვეობით" + "ამოცანათა ზოლის მუდმივად ჩვენება" + "თქვენი ეკრანის ქვედა ნაწილში ამოცანათა ზოლის მუდმივად საჩვენებლად, ხანგრძლივად შეეხეთ გამყოფს" "დახურვა" "მზადაა" "მთავარი" diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml index da3670e0df..ad944c5785 100644 --- a/quickstep/res/values-kk/strings.xml +++ b/quickstep/res/values-kk/strings.xml @@ -49,6 +49,7 @@ "Экранның оң немесе сол жиегінен сырғытыңыз." "Экранның оң немесе сол жиегінен ортасына қарай сырғытып, саусағыңызды жіберіңіз." "Оңнан солға сырғыту арқылы артқа қайтуды үйрендіңіз. Енді қолданбаларды ауыстыруды үйреніңіз." + "Артқа қайту қимылын аяқтадыңыз. Енді қолданбаларды ауыстыруды үйреніңіз." "Артқа қайту қимылын аяқтадыңыз." "Сырғытқанда саусақты экранның төменгі жағына қатты жақындатпаңыз." "Артқа қайту қимылы сезгіштігін параметрлерден өзгертіңіз." @@ -95,6 +96,7 @@ "Скриншот" "Бөлу" "Экранды бөлу режимін пайдалану үшін басқа қолданбаны түртіңіз." + "Экранды бөлу үшін басқа қолданбаны таңдаңыз." "Бас тарту" "Экранды бөлу режимінен шығу" "Экранды бөлу үшін басқа қолданбаны таңдаңыз." @@ -109,8 +111,10 @@ "2 қолданбаны бір мезгілде пайдалану үшін қолданбаны шетке сүйреңіз." "Тапсырмалар жолағын көрсету үшін жоғары қарай ақырын сырғытыңыз." "Іс-әрекеттеріңізге негізделген қолданба ұсыныстарын алыңыз." - "Тапсырмалар жолағын бекіту үшін бөлгішті ұзақ басып тұрыңыз" + "Тапсырмалар жолағын бекіту үшін бөлгішті ұзақ басып тұрыңыз." "Тапсырмалар жолағында мүмкіндік көп" + "Тапсырмалар жолағын әрдайым көрсету" + "Экранның төменгі жағында тапсырмалар жолағы әрдайым көрсетілуі үшін, бөлгішті басып тұрыңыз." "Жабу" "Дайын" "Негізгі экран" diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml index 4c3d7323e1..3a56606332 100644 --- a/quickstep/res/values-km/strings.xml +++ b/quickstep/res/values-km/strings.xml @@ -49,6 +49,7 @@ "ត្រូវប្រាកដថា​អ្នកអូសពី​គែមខាងស្ដាំ ឬ​ខាងឆ្វេង" "ត្រូវប្រាកដថា​អ្នកអូសពី​គែមខាងស្ដាំ ឬខាងឆ្វេង​ទៅផ្នែកកណ្ដាល​នៃអេក្រង់ រួច​ដកដៃ" "អ្នកបានស្វែងយល់ពីរបៀបអូសពីខាងស្ដាំ ដើម្បីថយក្រោយ។ បន្ទាប់​ទៀត សូមស្វែងយល់​ពីរបៀប​ប្ដូរកម្មវិធី​។" + "អ្នក​បានបញ្ចប់​ចលនា​ថយក្រោយ​ហើយ។ បន្ទាប់​មកទៀត សូមស្វែងយល់​ពីរបៀប​ប្ដូរកម្មវិធី​។" "អ្នក​បានបញ្ចប់​ចលនា​ថយក្រោយ​ហើយ" "ត្រូវប្រាកដថា​អ្នកមិនអូស​ទៅជិត​ផ្នែកខាងក្រោម​នៃអេក្រង់ពេក" "ដើម្បីប្ដូរកម្រិត​រំញោចនឹង​ចលនាថយក្រោយ សូមចូលទៅកាន់​ការកំណត់" @@ -95,6 +96,7 @@ "រូបថតអេក្រង់" "បំបែក" "ចុចកម្មវិធី​ផ្សេងទៀត ដើម្បីប្រើ​មុខងារបំបែកអេក្រង់" + "ជ្រើសរើសកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារ​បំបែកអេក្រង់" "បោះបង់" "ចាកចេញពីការជ្រើសរើសរបស់មុខងារ​បំបែកអេក្រង់" "ជ្រើសរើសកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារ​បំបែកអេក្រង់" @@ -111,6 +113,8 @@ "ទទួលការណែនាំកម្មវិធីដោយផ្អែកលើទម្លាប់របស់អ្នក" "ចុចឱ្យយូរនៅលើបន្ទាត់ខណ្ឌចែក ដើម្បីខ្ទាស់របារកិច្ចការ" "ធ្វើបានកាន់តែច្រើនដោយប្រើរបារកិច្ចការ" + "បង្ហាញរបារកិច្ចការជានិច្ច" + "ដើម្បីបង្ហាញរបារកិច្ចការនៅផ្នែកខាងក្រោមនៃអេក្រង់របស់អ្នកជានិច្ច សូមចុចបន្ទាត់ខណ្ឌចែកឱ្យជាប់" "បិទ" "រួចរាល់" "ទំព័រដើម" diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml index c8efb7e366..6759404c4e 100644 --- a/quickstep/res/values-kn/strings.xml +++ b/quickstep/res/values-kn/strings.xml @@ -49,6 +49,7 @@ "ನೀವು ಬಲಕೊನೆಯ ಅಂಚಿನಿಂದ ಅಥವಾ ಎಡಕೊನೆಯ ಅಂಚಿನಿಂದ ಸ್ವೈಪ್ ಮಾಡುತ್ತಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ" "ನೀವು ಬಲ ಅಥವಾ ಎಡ ಅಂಚಿನಿಂದ ಸ್ಕ್ರೀನ್‌ನ ಮಧ್ಯಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡುತ್ತಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಂಡು ಬಿಟ್ಟುಬಿಡಿ" "ಹಿಂದೆ ಹೋಗಲು ಬಲದಿಂದ ಸ್ವೈಪ್ ಮಾಡುವುದು ಹೇಗೆಂದು ಕಲಿತಿರಿ. ಮುಂದೆ, ಆ್ಯಪ್‌ಗಳನ್ನು ಬದಲಿಸುವುದು ಹೇಗೆಂದು ತಿಳಿಯಿರಿ." + "ನೀವು ಗೋ ಬ್ಯಾಕ್ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ. ಮುಂದೆ, ಆ್ಯಪ್‌ಗಳನ್ನು ಬದಲಾಯಿಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ." "ನೀವು ಗೋ ಬ್ಯಾಕ್ ಗೆಸ್ಚರ್ ಅನ್ನು ಪೂರ್ಣಗೊಳಿಸಿದ್ದೀರಿ" "ನೀವು ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಭಾಗಕ್ಕೆ ಹೆಚ್ಚು ಹತ್ತಿರ ಸ್ವೈಪ್ ಮಾಡದಂತೆ ನೋಡಿಕೊಳ್ಳಿ" "ಬ್ಯಾಕ್ ಗೆಸ್ಚರ್‌ನ ಸೂಕ್ಷ್ಮತೆ ಬದಲಾಯಿಸಲು, ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ ಹೋಗಿ" @@ -95,6 +96,7 @@ "ಸ್ಕ್ರೀನ್‌ಶಾಟ್" "ವಿಭಜಿಸಿ" "ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಟ್ಯಾಪ್ ಮಾಡಿ" + "ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಇನ್ನೊಂದು ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ" "ರದ್ದುಮಾಡಿ" "ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ" "\"ಪರದೆ ಬೇರ್ಪಡಿಸಿ\" ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ" @@ -107,10 +109,12 @@ "ಸ್ಕ್ರೀನ್ ತಿರುಗಿಸಿ" "ಟಾಸ್ಕ್‌ಬಾರ್ ಶಿಕ್ಷಣ" "ಒಂದೇ ಬಾರಿಗೆ 2 ಆ್ಯಪ್‌ಗಳನ್ನು ಬಳಸಲು ಆ್ಯಪ್ ಅನ್ನು ಬದಿಗೆ ಎಳೆಯಿರಿ" - "ಟಾಸ್ಕ್‌ಬಾರ್ ಅನ್ನು ತೋರಿಸಲು ನಿಧಾನವಾಗಿ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ" + "ಟಾಸ್ಕ್‌ಬಾರ್ ಕಾಣುವಂತೆ ಮಾಡಲು ನಿಧಾನವಾಗಿ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ" "ನಿಮ್ಮ ದಿನಚರಿಯ ಆಧಾರದ ಮೇಲೆ ಆ್ಯಪ್ ಸಲಹೆಗಳನ್ನು ಪಡೆಯಿರಿ" - "ಟಾಸ್ಕ್ ಬಾರ್ ಅನ್ನು ಪಿನ್ ಮಾಡಲು ಡಿವೈಡರ್ ಮೇಲೆ ದೀರ್ಘಕಾಲ ಒತ್ತಿರಿ" + "ಟಾಸ್ಕ್‌‌ಬಾರ್ ಅನ್ನು ಪಿನ್ ಮಾಡಲು ಡಿವೈಡರ್ ಮೇಲೆ ದೀರ್ಘಕಾಲ ಒತ್ತಿರಿ" "ಟಾಸ್ಕ್‌ಬಾರ್ ಮೂಲಕ ಹೆಚ್ಚಿನದನ್ನು ಮಾಡಿ" + "ಯಾವಾಗಲೂ ಟಾಸ್ಕ್‌ಬಾರ್ ಅನ್ನು ತೋರಿಸಿ" + "ಯಾವಾಗಲೂ ನಿಮ್ಮ ಸ್ಕ್ರೀನ್‌ನ ಕೆಳಭಾಗದಲ್ಲಿ ಟಾಸ್ಕ್ ಬಾರ್ ಅನ್ನು ತೋರಿಸಲು, ಡಿವೈಡರ್ ಅನ್ನು ಸ್ಪರ್ಶಿಸಿ ಹಿಡಿದಿಟ್ಟುಕೊಳ್ಳಿ" "ಮುಚ್ಚಿರಿ" "ಆಯಿತು" "ಮುಖಪುಟ" diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml index 5b5b728027..b83bfa2e0b 100644 --- a/quickstep/res/values-ko/strings.xml +++ b/quickstep/res/values-ko/strings.xml @@ -49,6 +49,7 @@ "오른쪽 또는 왼쪽 가장자리 끝에서 스와이프하세요." "오른쪽 또는 왼쪽 가장자리에서 화면 중앙으로 스와이프한 후 손가락을 떼세요." "오른쪽에서 스와이프하여 뒤로 돌아가는 방법을 배웠습니다. 이번에는 앱 전환 방법을 알아보겠습니다." + "돌아가기 동작을 완료했습니다. 이번에는 앱 전환 방법을 알아보겠습니다." "돌아가기 동작을 완료했습니다." "화면 하단에 지나치게 가까운 곳에서 스와이프하면 안 됩니다." "돌아가기 동작의 민감도를 변경하려면 설정으로 이동하세요" @@ -95,6 +96,7 @@ "스크린샷" "분할" "다른 앱을 탭하여 화면 분할 사용" + "화면 분할을 사용하려면 다른 앱을 선택하세요." "취소" "화면 분할 선택 종료" "화면 분할을 사용하려면 다른 앱을 선택하세요." @@ -109,8 +111,10 @@ "앱을 옆으로 드래그하여 앱 2개를 동시에 사용합니다." "위로 천천히 스와이프하면 태스크 바가 표시됩니다." "사용 습관에 따라 앱 제안을 받습니다." - "구분선을 길게 눌러 태스크 바 고정하기" + "구분선을 길게 눌러 태스크 바를 고정합니다." "태스크 바 최대한 활용하기" + "태스크 바 항상 표시" + "화면 하단에 태스크 바를 항상 표시하려면 구분선을 길게 터치하세요." "닫기" "완료" "홈" diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml index ea98e71513..9920b028f7 100644 --- a/quickstep/res/values-ky/strings.xml +++ b/quickstep/res/values-ky/strings.xml @@ -49,6 +49,7 @@ "Экранды эң четинен оңдон солго же солдон оңго карай сүрүңүз" "Экранды оң же сол жагынан ортосуна карай сүрүп, манжаңызды алыңыз" "Артка кайтуу үчүн экранды оңдон солго карай сүрүүнү үйрөндүңүз. Эми колдонмолорду которуштурганды үйрөнүп алыңыз." + "\"Артка\" жаңсоосун үйрөндүңүз. Эми колдонмолорду которуштурганды үйрөнүп алыңыз." "\"Артка\" жаңсоосун үйрөндүңүз" "Манжаңызды экрандын ылдый жагына өтө жакындатпай сүрүңүз" "\"Артка\" жаң-нун сезгичтигин өзгөртүү үчүн параметрлерге өтүңүз" @@ -95,6 +96,7 @@ "Скриншот" "Бөлүү" "Экранды бөлүү үчүн башка колдонмону таптап коюңуз" + "Экранды бөлүү үчүн башка колдонмону тандаңыз" "Жокко чыгаруу" "Тандалган экранды бөлүүдөн чыгуу" "Экранды бөлүү үчүн башка колдонмону тандаңыз" @@ -106,11 +108,13 @@ "Өткрп жиберүү" "Экранды буруу" "Тапшырмалар тактасы жөнүндө маалымат" - "2 колдонмону бир убакта пайдалануу үчүн капталга сүйрөңүз" + "2 колдонмону бир убакта пайдалануу үчүн капталга сүйрөйсүз" "Тапшырмалар тактасын көрүү үчүн экранды жай өйдө сүрүңүз" - "Программаңыздын негизинде сунушталган колдонмолорду алуу" - "Тапшырмалар панелин кадап коюу үчүн бөлгүчтү коё бербей басып туруңуз" - "Тапшырмалар тактасы менен көбүрөөк нерселерди аткарыңыз" + "Аракеттериңизге негизделген сунуштарды алып турасыз" + "Тапшырмалар тактасын кадап коюу үчүн бөлгүчтү коё бербей басып турасыз" + "Тапшырмалар тактасы менен көбүрөөк иш бүтүрөсүз" + "Тапшырмалар панелин ар дайым көрсөтүү" + "Тапшырмалар панелин экрандын ылдый жагында ар дайым көрсөтүү үчүн бөлгүчтү коё бербей басыңыз" "Жабуу" "Бүттү" "Башкы бет" @@ -124,7 +128,7 @@ "Тапшырмалар панели көрсөтүлдү" "Тапшырмалар панели жашырылды" "Чабыттоо тилкеси" - "Тапшырмалар панелин ар дайым көрсөтүү" + "Такта ар дайым көрүнсүн" "Өтүү режимин өзгөртүү" "Тапшырмалар панелин бөлгүч" "Жогорку/сол бурчка жылдыруу" diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml index 635523e89a..fa72bd1fd3 100644 --- a/quickstep/res/values-lo/strings.xml +++ b/quickstep/res/values-lo/strings.xml @@ -49,6 +49,7 @@ "ກະລຸນາກວດສອບວ່າທ່ານປັດຈາກຂອບຂວາສຸດ ຫຼື ຊ້າຍສຸດ" "ກະລຸນາກວດສອບວ່າທ່ານປັດຈາກຂອບຂວາ ຫຼື ຊ້າຍໄປຫາທາງກາງຂອງໜ້າຈໍແລ້ວປ່ອຍນິ້ວ" "ທ່ານຮຽນຮູ້ວິທີປັດຈາກຂວາເພື່ອກັບຄືນແລ້ວ. ຕໍ່ໄປ, ມາສຶກສາວິທີສະຫຼັບແອັບ." + "ທ່ານໃຊ້ທ່າທາງກັບຄືນສຳເລັດແລ້ວ. ຕໍ່ໄປ, ມາສຶກສາວິທີສະຫຼັບແອັບ." "ທ່ານໃຊ້ທ່າທາງກັບຄືນສຳເລັດແລ້ວ" "ກະລຸນາກວດສອບວ່າທ່ານບໍ່ໄດ້ປັດໃກ້ກັບທາງລຸ່ມຂອງໜ້າຈໍເກີນໄປ" "ເພື່ອປ່ຽນຄວາມລະອຽດອ່ອນຂອງທ່າທາງກັບຄືນ, ໃຫ້ໄປຫາການຕັ້ງຄ່າ" @@ -95,6 +96,7 @@ "ຮູບໜ້າຈໍ" "ແບ່ງ" "ແຕະແອັບອື່ນເພື່ອໃຊ້ໜ້າຈໍແຍກ" + "ເລືອກແອັບອື່ນເພື່ອໃຊ້ການແບ່ງໜ້າຈໍ" "ຍົກເລີກ" "ອອກຈາກາກນເລືອກການແບ່ງໜ້າຈໍ" "ເລືອກແອັບອື່ນເພື່ອໃຊ້ການແບ່ງໜ້າຈໍ" @@ -111,6 +113,8 @@ "ຮັບການແນະນຳແອັບໂດຍອີງໃສ່ສິ່ງທີ່ເຮັດປະຈຳຂອງທ່ານ" "ກົດຕົວຂັ້ນຄ້າງໄວ້ເພື່ອປັກໝຸດແຖບໜ້າວຽກ" "ເຮັດສິ່ງຕ່າງໆໄດ້ຫຼາຍຂຶ້ນດ້ວຍແຖບໜ້າວຽກ" + "ສະແດງແຖບໜ້າວຽກສະເໝີ" + "ເພື່ອໃຫ້ແຖບໜ້າວຽກສະແດງຢູ່ລຸ່ມໜ້າຈໍຂອງທ່ານຢູ່ສະເໝີ, ໃຫ້ແຕະຕົວແບ່ງຄ້າງໄວ້" "ປິດ" "ແລ້ວໆ" "ໜ້າຫຼັກ" diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml index c5839e3679..6e712349ed 100644 --- a/quickstep/res/values-lt/strings.xml +++ b/quickstep/res/values-lt/strings.xml @@ -49,6 +49,7 @@ "Turite perbraukti nuo dešiniojo ar kairiojo krašto" "Turite perbraukti nuo dešiniojo ar kairiojo krašto link ekrano vidurio ir pakelti pirštą" "Išmokote, kaip sugrįžti perbraukiant iš dešinės. Toliau sužinosite, kaip perjungti programas." + "Atlikote grįžimo atgal gestą. Toliau sužinosite, kaip perjungti programas." "Atlikote grįžimo atgal gestą" "Nebraukite per arti ekrano apačios" "Norėd. pak. grįžimo gesto jautr., eikite į sk. „Nustatymai“" @@ -95,6 +96,7 @@ "Ekrano kopija" "Išskaidymo režimas" "Išskaidyto ekrano režimas palietus kitą programą" + "Išskaidyto ekrano režimą naudokite kita programa" "Atšaukti" "Išeiti iš išskaidyto ekrano pasirinkimo" "Išskaidyto ekrano režimą naudokite kita programa" @@ -111,6 +113,8 @@ "Gaukite programų pasiūlymų pagal savo veiklą" "Ilgai paspauskite daliklį, kad prisegtumėte užduočių juostą" "Atlikite daugiau naudodami Užduočių juostą" + "Visada rodyti užduočių juostą" + "Jei norite, kad užduočių juosta visada būtų rodoma ekrano apačioje, palieskite ir palaikykite daliklį" "Uždaryti" "Atlikta" "Pagrindinis" diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml index 063a6267ce..8fd7b1249a 100644 --- a/quickstep/res/values-lv/strings.xml +++ b/quickstep/res/values-lv/strings.xml @@ -49,6 +49,7 @@ "Jāvelk no pašas labās vai kreisās malas." "Jāvelk no ekrāna labās vai kreisās malas uz vidu un jāatlaiž." "Jūs esat apguvis, kā vilkt no labās malas, lai pārietu atpakaļ. Tagad mācieties pārslēgt lietotnes." + "Jūs sekmīgi veicāt atgriešanās žestu. Tagad varat iemācīties, kā pārslēgt lietotnes." "Jūs sekmīgi veicāt atgriešanās žestu." "Nevelciet pārāk tuvu ekrāna apakšdaļai." "Atgriešanās žesta jutīguma līmeni varat mainīt iestatījumos." @@ -95,6 +96,7 @@ "Veikt ekrānuzņēmumu" "Sadalīt" "Lai sadalītu ekrānu, pieskarieties citai lietotnei" + "Izvēlieties citu lietotni, lai sadalītu ekrānu" "Atcelt" "Izejiet no ekrāna sadalīšanas režīma atlases." "Izvēlieties citu lietotni, lai sadalītu ekrānu" @@ -111,6 +113,8 @@ "Skatiet ieteiktās lietotnes, balstoties uz jūsu ieradumiem" "Nospiediet/turiet atdalītāju, lai piespraustu uzdevumu joslu" "Plašākas iespējas, izmantojot uzdevumu joslu" + "Vienmēr rādīt uzdevumu joslu" + "Lai uzdevumu joslu rādītu apakšdaļā, pieskarieties atdalītājam un turiet" "Aizvērt" "Gatavs" "Sākums" diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml index 61d34dd830..a36f7b5f5c 100644 --- a/quickstep/res/values-mk/strings.xml +++ b/quickstep/res/values-mk/strings.xml @@ -49,6 +49,7 @@ "Повлечете од крајниот десен или крајниот лев раб" "Повлечете од десниот или левиот раб кон средината на екранот и пуштете" "Научивте како да повлекувате оддесно за враќање назад. Научете и како да се префрлате помеѓу апликациите." + "Го научивте движењето за враќање назад. Научете го и движењето за префрлање помеѓу апликациите." "Завршивте со упатството за враќање назад" "Не повлекувајте преблиску до дното на екранот" "За да ја промените чувствителноста, одете во „Поставки“" @@ -95,6 +96,7 @@ "Слика од екранот" "Раздели" "Допрете друга аплик. за да користите поделен екран" + "Изберете друга апликација за да користите поделен екран" "Откажи" "Излези од изборот на поделен екран" "Изберете друга апликација за да користите поделен екран" @@ -106,11 +108,13 @@ "Прескокни" "Ротирајте го екранот" "Обука за лентата со задачи" - "Повлечете апликација настрана за да користите 2 апликации" - "Полека повлечете нагоре за да се прикаже лентата со задачи" + "Повлечете апликација настрана за да користите 2 апликации одеднаш" + "Полека повлечете нагоре за да се прикаже „Лентата со задачи“" "Добивајте предлози за апликации според вашата рутина" "Притиснете долго на разделникот за да ја закачите „Лентата со задачи“" - "Правете повеќе со една лента со задачи" + "Правете сешто со „Лентата со задачи“" + "Како секогаш да се прикажува „Лентата со задачи“" + "Допрете и задржете го разделникот за да може „Лентата со задачи“ секогаш да се прикажува на дното на екранот" "Затвори" "Готово" "Дома" diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml index f9e78271ba..3e6d07512d 100644 --- a/quickstep/res/values-ml/strings.xml +++ b/quickstep/res/values-ml/strings.xml @@ -49,6 +49,7 @@ "വലത്തേയറ്റത്തെയോ ഇടത്തേയറ്റത്തെയോ അരികിൽ നിന്നാണ് സ്വെെപ്പ് ചെയ്യുന്നതെന്ന് ഉറപ്പാക്കുക" "വലതോ ഇടതോ അരികിൽ നിന്ന് സ്‌ക്രീനിന്റെ മധ്യഭാഗത്തേക്കാണ് സ്വെെപ്പ് ചെയ്യുന്നതെന്ന് ഉറപ്പാക്കുക" "മടങ്ങാൻ വലതുഭാഗത്ത് നിന്ന് സ്വൈപ്പ് ചെയ്യുന്ന രീതി മനസ്സിലായി. ഇനി, ആപ്പുകൾ മാറുന്ന രീതി അറിയുക." + "മടങ്ങുക ജെസ്ച്ചർ നിങ്ങൾ പൂർത്തിയാക്കി. അടുത്തത്, എങ്ങനെ ആപ്പുകൾ തമ്മിൽ മാറാമെന്ന് മനസ്സിലാക്കുക." "മടങ്ങുക ജെസ്ച്ചർ നിങ്ങൾ പൂർത്തിയാക്കി" "സ്‌ക്രീനിന്റെ ഏറ്റവും അടിഭാഗത്തേക്ക് സ്വെെപ്പ് ചെയ്യുന്നില്ലെന്ന് ഉറപ്പാക്കുക" "ബാക്ക്ജെസ്റ്ററിന്റെ സെൻസിറ്റിവിറ്റി മാറ്റാൻ ക്രമീകരണത്തിൽ പോകൂ" @@ -95,6 +96,7 @@ "സ്ക്രീൻഷോട്ട്" "വിഭജിക്കുക" "സ്പ്ലിറ്റ് സ്ക്രീനിന് മറ്റൊരു ആപ്പിൽ ടാപ്പ് ചെയ്യൂ" + "സ്ക്രീൻ വിഭജന മോഡ് ഉപയോഗിക്കാൻ മറ്റൊരു ആപ്പ് തിരഞ്ഞെടുക്കൂ" "റദ്ദാക്കുക" "സ്‌ക്രീൻ വിഭജന തിരഞ്ഞെടുപ്പിൽ നിന്ന് പുറത്തുകടക്കുക" "സ്ക്രീൻ വിഭജന മോഡിന് മറ്റൊരു ആപ്പ് തിരഞ്ഞെടുക്കൂ" @@ -111,6 +113,8 @@ "നിങ്ങളുടെ ദിനചര്യ അനുസരിച്ച് ആപ്പ് നിർദ്ദേശങ്ങൾ നേടുക" "ടാസ്‌ക്ബാർ പിൻ ചെയ്യാൻ ഡിവൈഡറിൽ ദീർഘനേരം അമർത്തുക" "ടാസ്‌ക്‌ബാർ ഉപയോഗിച്ച് കൂടുതൽ ചെയ്യുക" + "എല്ലായ്‌പ്പോഴും ടാസ്‌ക്‌ബാർ കാണിക്കുക" + "ടാസ്‌ക്ബാർ എല്ലായ്‌പ്പോഴും നിങ്ങളുടെ സ്‌ക്രീനിന്റെ ചുവടെ കാണിക്കുന്നതിന് ഡിവൈഡറിൽ സ്‌പർശിച്ച് പിടിക്കുക" "അടയ്ക്കുക" "പൂർത്തിയായി" "ഹോം" diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml index 1b8694892a..71e7fb5948 100644 --- a/quickstep/res/values-mn/strings.xml +++ b/quickstep/res/values-mn/strings.xml @@ -49,6 +49,7 @@ "Та баруун зах эсвэл зүүн захын ирмэгээс шударна уу" "Та баруун эсвэл зүүн ирмэгээс дэлгэцийн дунд хэсэг хүртэл шударч, суллана уу" "Та буцахын тулд баруунаас хэрхэн шудрахыг мэдэж авлаа. Дараа нь аппууд хооронд хэрхэн сэлгэхийг мэдэж аваарай." + "Та буцах зангааг гүйцэтгэлээ. Дараа нь аппуудыг хэрхэн сэлгэх талаар мэдэж авна уу." "Та буцах зангааг гүйцэтгэлээ" "Та дэлгэцийн доод хэсэгтэй хэт ойр бүү шудраарай" "Буцах зангааны мэдрэгшлийг өөрчлөх бол Тохиргоо руу очно уу" @@ -95,6 +96,7 @@ "Дэлгэцийн агшин дарах" "Хуваах" "Дэлгэцийг хуваахыг ашиглахын тулд өөр аппыг товш" + "Дэлгэц хуваахыг ашиглахын тулд өөр апп сонгоно уу" "Цуцлах" "Дэлгэцийг хуваах сонголтоос гарах" "Дэлгэцийг хуваах горим ашиглах өөр апп сонгоно уу" @@ -111,6 +113,8 @@ "Таны хэвшилд тулгуурлан санал болгож буй аппуудыг аваарай" "Ажлын хэсгийг бэхлэхийн тулд тусгаарлагчийг удаан дарна уу" "Ажлын хэсгийн тусламжтай илүү ихийг хийгээрэй" + "Ажлын хэсгийг үргэлж харуулах" + "Дэлгэцийнхээ доод талд Ажлын хэсгийг үргэлж харуулахын тулд хуваагч дээр хүрээд удаан дарна уу" "Хаах" "Дууссан" "Гэр" diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml index 722e17d781..eea6f85cee 100644 --- a/quickstep/res/values-mr/strings.xml +++ b/quickstep/res/values-mr/strings.xml @@ -49,6 +49,7 @@ "तुम्ही स्क्रीनच्या अगदी उजव्या किंवा अगदी डाव्या कडेपासून स्‍वाइप करत आहात खात्री करा" "तुम्ही स्क्रीनच्या उजव्या किंवा डाव्या कडेपासून मध्यभागी स्‍वाइप करून सोडून देत आहात याची खात्री करा" "मागे जाण्यासाठी उजवीकडून कसे स्‍वाइप करावे ते शिकलात. आता पुढे, ॲप्स कशी स्विच करायची ते जाणून घ्या." + "तुम्ही गो बॅक जेश्चर पूर्ण केले. आता, ॲप्स कशी स्विच करायची ते जाणून घ्या." "तुम्ही गो बॅक जेश्चर पूर्ण केले आहे" "तुम्ही स्क्रीनच्या तळाच्या अगदी जवळून स्‍वाइप करत नाही याची खात्री करा" "बॅक जेश्चरची संवेदनशीलता बदलण्यासाठी, सेटिंग्ज वर जा" @@ -95,6 +96,7 @@ "स्क्रीनशॉट" "स्प्लिट" "स्प्लिट स्क्रीन वापरण्यासाठी दुसऱ्या ॲपवर टॅप करा" + "स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा" "रद्द करा" "स्प्लिट स्क्रीन निवडीतून बाहेर पडा" "स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा" @@ -111,6 +113,8 @@ "तुमच्या दिनक्रमावर आधारित ॲप सूचना मिळवा" "टास्कबार पिन करण्यासाठी विभाजकावर प्रेस करून ठेवा" "टास्कबार चा पुरेपूर वापर करा" + "टास्कबार नेहमी दाखवा" + "टास्कबार नेहमी तुमच्या स्क्रीनच्या तळाशी दाखवण्यासाठी, विभाजकाला स्पर्श करून धरून ठेवा" "बंद करा" "पूर्ण झाले" "होम" diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml index 1959cd76f3..09b15b51da 100644 --- a/quickstep/res/values-ms/strings.xml +++ b/quickstep/res/values-ms/strings.xml @@ -49,6 +49,7 @@ "Pastikan anda meleret dari hujung sebelah kanan atau hujung sebelah kiri" "Pastikan anda meleret dari tepi sebelah kanan atau kiri ke bahagian tengah skrin dan lepaskan" "Anda sudah belajar cara meleret dari kanan untuk kembali. Seterusnya, ketahui cara menukar apl." + "Anda telah melengkapkan gerak isyarat undur. Seterusnya, ketahui cara menukar apl." "Anda telah melengkapkan gerak isyarat undur" "Pastikan anda tidak meleret terlalu dekat dengan bahagian bawah skrin" "Utk mengubah kepekaan gerak isyarat undur, pergi ke Tetapan" @@ -95,6 +96,7 @@ "Tangkapan skrin" "Pisah" "Ketik apl lain untuk menggunakan skrin pisah" + "Pilih apl lain untuk menggunakan skrin pisah" "Batal" "Keluar daripada pilihan skrin pisah" "Pilih apl lain untuk menggunakan skrin pisah" @@ -111,6 +113,8 @@ "Dapatkan cadangan apl berdasarkan rutin anda" "Tekan lama pada pembahagi untuk menyematkan Bar Tugas" "Lakukan lebih banyak perkara dengan Bar Tugas" + "Sentiasa paparkan Bar Tugas" + "Untuk sentiasa memaparkan Bar Tugas pada bahagian bawah skrin, sentuh & tahan pembahagi" "Tutup" "Selesai" "Laman Utama" @@ -124,7 +128,7 @@ "Bar Tugas dipaparkan" "Bar Tugas disembunyikan" "Bar navigasi" - "Sentiasa paparkan Bar Tugas" + "Papar Bar Tugas selalu" "Tukar mod navigasi" "Pembahagi Bar Tugas" "Alihkan ke atas/kiri" diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml index 96ef54ab2e..e8f2e1fda2 100644 --- a/quickstep/res/values-my/strings.xml +++ b/quickstep/res/values-my/strings.xml @@ -49,6 +49,7 @@ "ညာ (သို့) ဘယ်အစွန်း၏ ခပ်လှမ်းလှမ်းမှ ပွတ်ဆွဲကြောင်း သေချာပါစေ" "ဖန်သားပြင်၏ ညာ (သို့) ဘယ်အစွန်းမှ အလယ်သို့ ပွတ်ဆွဲပြီး လွှတ်လိုက်ကြောင်း သေချာပါစေ" "နောက်ပြန်သွားရန် ညာဘက်မှပွတ်ဆွဲနည်းကို သိသွားပါပြီ။ နောက်အဆင့်တွင် အက်ပ်များပြောင်းနည်းကို လေ့လာပါ။" + "နောက်ဆုတ်လက်ဟန် ရှင်းလင်းပို့ချချက် ပြီးပါပြီ။ နောက်အဆင့်တွင် အက်ပ်များပြောင်းနည်းကို လေ့လာပါ။" "နောက်သို့ လက်ဟန် အပြီးသတ်လိုက်ပါပြီ" "ဖန်သားပြင် အောက်ခြေနှင့် အလွန်နီးကပ်စွာ ပွတ်ဆွဲခြင်းမရှိကြောင်း သေချာပါစေ" "နောက်ဆုတ်လက်ဟန်၏ အာရုံခံစွမ်းကိုပြောင်းရန် ‘ဆက်တင်များ’ သို့ သွားပါ" @@ -95,6 +96,7 @@ "ဖန်သားပြင်ဓာတ်ပုံ" "ခွဲထုတ်ရန်" "မျက်နှာပြင် ခွဲ၍ပြသရန် အက်ပ်နောက်တစ်ခုကို တို့ပါ" + "မျက်နှာပြင် ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ" "မလုပ်တော့" "မျက်နှာပြင် ခွဲ၍ပြသခြင်း ရွေးချယ်မှုမှ ထွက်ရန်" "မျက်နှာပြင်ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ" @@ -106,11 +108,13 @@ "ကျော်ရန်" "ဖန်သားပြင်လှည့်ရန်" "လုပ်ဆောင်စရာဘား ပညာပေး" - "အက်ပ် ၂ ခု တစ်ပြိုင်တည်းသုံးရန် အက်ပ်ကို ဘေးသို့ ဖိဆွဲပါ" + "အက်ပ် ၂ ခု တစ်ပြိုင်တည်းသုံးရန် အက်ပ်တစ်ခုကို ဘေးသို့ဖိဆွဲပါ" "Taskbar ပြရန် အပေါ်သို့ ဖြည်းဖြည်းပွတ်ဆွဲပါ" "ပုံမှန်အစီအစဉ်ပေါ် အခြေခံ၍ အက်ပ်အကြံပြုချက်များကို ရယူပါ" - "Taskbar ပင်ထိုးရန် ခွဲခြားမျဉ်းကို ဖိနှိပ်ပါ" + "Taskbar ပင်ထိုးရန် ခွဲခြားမျဉ်းကို နှိပ်ထားပါ" "Taskbar ဖြင့် ပိုမိုလုပ်ဆောင်နိုင်ခြင်း" + "Taskbar ကို အမြဲပြပါ" + "Taskbar ကို စခရင်အောက်ခြေတွင် အမြဲပြရန် ခွဲခြားမျဉ်းကို တို့ထိ၍ ဖိထားပါ" "ပိတ်ရန်" "ပြီးပြီ" "ပင်မစာမျက်နှာ" @@ -124,7 +128,7 @@ "Taskbar ပြထားသည်" "Taskbar ဖျောက်ထားသည်" "လမ်းညွှန်ဘား" - "‘လုပ်ဆောင်စရာဘား’ အမြဲပြပါ" + "Taskbar အမြဲပြရန်" "ရွှေ့ကြည့်သည့်မုဒ် ပြောင်းရန်" "လုပ်ဆောင်စရာဘား ပိုင်းခြားစနစ်" "အပေါ်/ဘယ်ဘက်သို့ ရွှေ့ရန်" diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml index f129361965..ffbb73cf88 100644 --- a/quickstep/res/values-nb/strings.xml +++ b/quickstep/res/values-nb/strings.xml @@ -49,6 +49,7 @@ "Sørg for at du sveiper fra kanten helt til høyre eller venstre" "Sørg for at du sveiper fra den høyre eller venstre kanten til midten av skjermen og slipper" "Du har lært hvordan du sveiper fra høyre for å gå tilbake. I neste trinn lærer du å bytte app." + "Du har fullført bevegelsen for å gå tilbake. I neste trinn lærer du hvordan du bytter app." "Du har fullført bevegelsen for å gå tilbake" "Sørg for at du ikke sveiper for nær bunnen av skjermen" "Gå til Innstillinger for å endre tilbakebevegelsefølsomheten" @@ -95,6 +96,7 @@ "Skjermdump" "Del opp" "Trykk på en annen app for å bruke delt skjerm" + "Velg en annen app for å bruke delt skjerm" "Avbryt" "Avslutt valg av delt skjerm" "Velg en annen app for å bruke delt skjerm" @@ -111,6 +113,8 @@ "Få appforslag som er basert på rutinene dine" "Trykk lenge på skillelinjen for å feste oppgavelinjen" "Gjør mer med oppgavelinjen" + "Vis alltid oppgavelinjen" + "For å alltid vise oppgavelinjen nederst på skjermen, trykk og hold på skillelinjen" "Lukk" "Ferdig" "Hjem" diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml index 7ff28f6278..8e2e44f588 100644 --- a/quickstep/res/values-ne/strings.xml +++ b/quickstep/res/values-ne/strings.xml @@ -45,10 +45,11 @@ "सिफारिस गरिएका एपहरू देखाउने सुविधा असक्षम पारिएको छ" "पूर्वानुमान गरिएको एप: %1$s" "आफ्नो डिभाइस रोटेट गर्नुहोस्" - "इसारामार्फत गरिने नेभिगेसनको ट्युटोरियल पूरा गर्न कृपया आफ्नो डिभाइस रोटेट गर्नुहोस्" + "जेस्चर नेभिगेसनको ट्युटोरियल पूरा गर्न कृपया आफ्नो डिभाइस रोटेट गर्नुहोस्" "स्क्रिनको सबैभन्दा दायाँ किनारा वा सबैभन्दा बायाँ किनाराबाट स्वाइप गर्नुहोस्" "स्क्रिनको दायाँ वा बायाँ किनाराबाट मध्य भागसम्म स्वाइप गर्नुहोस् अनि औँला उठाउनुहोस्" "तपाईंले स्क्रिनको दायाँ किनाराबाट स्वाइप गरेर अघिल्लो स्क्रिनमा फर्कने तरिका सिक्नुभयो। अब एउटा एपबाट अर्को एपमा जाने तरिका सिक्नुहोस्।" + "तपाईंले \'पछाडि जानुहोस्\' नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो। अब एउटा एपबाट अर्को एपमा जाने तरिका सिक्नुहोस्।" "तपाईंले \"पछाडि जानुहोस्\" नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो" "स्क्रिनको फेदको धेरै नजिकसम्म स्वाइप नगर्नुहोस्" "\'पछाडि\' नामक इसाराको संवेदनशीलता बदल्न सेटिङमा जानुहोस्" @@ -71,7 +72,7 @@ "स्क्रिनको फेदबाट माथितिर स्वाइप गर्नुहोस्" "स्क्रिनबाट औँला उठाउनुअघि एपको विन्डोमा केही बेर छोइराख्नुहोस्" "सीधै माथितिर स्वाइप गर्नुहोस् अनि रोकिनुहोस्" - "तपाईंले इसाराहरू प्रयोग गर्ने तरिका सिक्नुभयो। इसारा अफ गर्न सेटिङमा जानुहोस्।" + "तपाईंले जेस्चरहरू प्रयोग गर्ने तरिका सिक्नुभयो। इसारा अफ गर्न सेटिङमा जानुहोस्।" "तपाईंले \"एउटा एपबाट अर्को एपमा जानुहोस्\" नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो" "एउटा एपबाट अर्को एपमा जान स्वाइप गर्नुहोस्" "एउटा एपबाट अर्कोमा जान स्क्रिनको फेदबाट माथितिर स्वाइप गर्नुहोस्, छोइराख्नुहोस् अनि औँला उठाउनुहोस्।" @@ -95,6 +96,7 @@ "स्क्रिनसट" "स्प्लिट गर्नुहोस्" "स्प्लिटस्क्रिन प्रयोग गर्न अर्को एपमा ट्याप गर्नु…" + "स्प्लिट स्क्रिन प्रयोग गर्न अर्को एप रोज्नुहोस्" "रद्द गर्नुहोस्" "स्प्लिट स्क्रिन मोडबाट बाहिरिनुहोस्" "स्प्लिट स्क्रिन प्रयोग गर्न अर्को एप रोज्नुहोस्" @@ -111,6 +113,8 @@ "आफ्नो रुटिनका आधारमा एपसम्बन्धी सुझावहरू प्राप्त गर्नुहोस्" "टास्कबार पिन गर्न डिभाइडरमा केही बेरसम्म थिच्नुहोस्" "टास्कबार प्रयोग गरेर अझ धेरै कार्य गर्नुहोस्" + "टास्कबार सधैँ देखाइयोस्" + "आफ्नो स्क्रिनको पुछारमा टास्कबार सधैँ देखाइराख्न डिभाइडर टच एन्ड होल्ड गर्नुहोस्" "बन्द गर्नुहोस्" "सम्पन्न भयो" "होम" diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml index bf10bf215c..12cadbbefd 100644 --- a/quickstep/res/values-nl/strings.xml +++ b/quickstep/res/values-nl/strings.xml @@ -49,6 +49,7 @@ "Swipe vanaf de rechter- of linkerrand" "Swipe vanaf de rechter- of linkerrand naar het midden van het scherm en laat los" "Je weet nu hoe je vanaf rechts kunt swipen om terug te gaan. Ontdek nu hoe je tussen apps schakelt." + "Je weet nu hoe je het gebaar Terug maakt. Ontdek als volgende hoe je tussen apps schakelt." "Je weet nu hoe je het gebaar Terug maakt" "Swipe niet te dicht bij de onderkant van het scherm" "Open Instellingen om de gevoeligheid van Terug te wijzigen" @@ -95,6 +96,7 @@ "Screenshot" "Splitsen" "Tik op nog een app om je scherm te splitsen" + "Kies een andere app om gesplitst scherm te gebruiken" "Annuleren" "Sluit de selectie voor gesplitst scherm" "Kies andere app om gesplitst scherm te gebruiken" @@ -111,6 +113,8 @@ "Krijg app-suggesties op basis van je routine" "Houd je vinger op de scheiding om de taakbalk vast te zetten" "Doe meer met de taakbalk" + "De taakbalk altijd tonen" + "Houd de scheidingslijn ingedrukt als je de taakbalk altijd onderaan je scherm wilt tonen" "Sluiten" "Klaar" "Home" diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml index d9f49e6b4f..9c32921c6a 100644 --- a/quickstep/res/values-or/strings.xml +++ b/quickstep/res/values-or/strings.xml @@ -49,6 +49,7 @@ "ଆପଣ ସ୍କ୍ରିନର ଏକଦମ୍-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ୍ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" "ଆପଣ ସ୍କ୍ରିନର ଡାହାଣ ବା ବାମ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରି ଛାଡ଼ି ଦେଉଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ଆପଣ ଡାହାଣରୁ ସ୍ୱାଇପ୍ କରି ପଛକୁ କିପରି ଫେରିବେ ତାହା ଜାଣିଲେ। ତା\'ପରେ, ଆପକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" + "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି। ତା\'ପରେ, ଆପଗୁଡ଼ିକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି" "ଆପଣ ସ୍କ୍ରିନର ତଳଭାଗର ଅତି ନିକଟରୁ ସ୍ୱାଇପ କରୁନଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ପଛକୁ ଫେରିବା ଜେଶ୍ଚରର ସମ୍ବେଦନଶୀଳତା ବଦଳାଇବାକୁ ସେଟିଂସକୁ ଯାଆନ୍ତୁ" @@ -95,6 +96,7 @@ "ସ୍କ୍ରିନସଟ୍" "ସ୍ପ୍ଲିଟ୍" "ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର" + "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" "ବାତିଲ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" @@ -111,6 +113,8 @@ "ଆପଣଙ୍କ ରୁଟିନ ଆଧାରରେ ଆପ ପରାମର୍ଶଗୁଡ଼ିକୁ ପାଆନ୍ତୁ" "ଟାସ୍କବାର ପିନ କରିବା ପାଇଁ ଡିଭାଇଡରକୁ ଅଧିକ ସମୟ ଦବାନ୍ତୁ" "ଟାସ୍କବାର ମାଧ୍ୟମରେ ଆହୁରି ଅନେକ କିଛି କରନ୍ତୁ" + "ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ" + "ଆପଣଙ୍କ ସ୍କ୍ରିନର ନିମ୍ନରେ ସର୍ବଦା ଟାସ୍କବାର ଦେଖାଇବା ପାଇଁ ଡିଭାଇଡରକୁ ସ୍ପର୍ଶ କରି ଧରି ରଖନ୍ତୁ" "ବନ୍ଦ କରନ୍ତୁ" "ହୋଇଗଲା" "ହୋମ" diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml index cdf4d299f5..e467303622 100644 --- a/quickstep/res/values-pa/strings.xml +++ b/quickstep/res/values-pa/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "ਪਿੰਨ ਕਰੋ" "ਫ੍ਰੀਫਾਰਮ" - "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮਾਂ ਨਹੀਂ" + "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮ ਨਹੀਂ" "ਐਪ ਵਰਤੋਂ ਦੀਆਂ ਸੈਟਿੰਗਾਂ" "ਸਭ ਕਲੀਅਰ ਕਰੋ" "ਹਾਲੀਆ ਐਪਾਂ" @@ -49,6 +49,7 @@ "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸੱਜੇ ਜਾਂ ਖੱਬੇ ਪਾਸੇ ਦੇ ਬਿਲਕੁਲ ਕਿਨਾਰੇ ਤੋਂ ਸਵਾਈਪ ਕਰਦੇ ਹੋ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸੱਜੇ ਜਾਂ ਖੱਬੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ ਸਵਾਈਪ ਕਰਦੇ ਹੋ ਅਤੇ ਛੱਡ ਦਿੰਦੇ ਹੋ" "ਤੁਸੀਂ ਪਿੱਛੇ ਜਾਣ ਲਈ ਸੱਜੇ ਪਾਸੇ ਤੋਂ ਸਵਾਈਪ ਕਰਨ ਦਾ ਤਰੀਕਾ ਜਾਣਿਆ। ਅੱਗੇ, ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰਨ ਦਾ ਤਰੀਕਾ ਜਾਣੋ।" + "ਤੁਸੀਂ \'ਵਾਪਸ ਜਾਓ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ। ਅੱਗੇ, ਜਾਣੋ ਕਿ ਐਪਾਂ ਵਿਚਾਲੇ ਅਦਲਾ-ਬਦਲੀ ਕਿਵੇਂ ਕਰਨੀ ਹੈ।" "ਤੁਸੀਂ \'ਵਾਪਸ ਜਾਓ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਲੇ ਹਿੱਸੇ ਦੇ ਬਹੁਤ ਨੇੜੇ ਸਵਾਈਪ ਨਾ ਕਰੋ" "ਪਿੱਛੇ ਜਾਣ ਦੇ ਸੰਕੇਤ ਦੀ ਸੰਵੇਦਨਸ਼ੀਲਤਾ ਬਦਲਣ ਲਈ, ਸੈਟਿੰਗਾਂ \'ਤੇ ਜਾਓ" @@ -95,6 +96,7 @@ "ਸਕ੍ਰੀਨਸ਼ਾਟ" "ਸਪਲਿਟ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ" + "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" "ਰੱਦ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" @@ -106,11 +108,13 @@ "ਛੱਡੋ" "ਸਕ੍ਰੀਨ ਘੁਮਾਓ" "ਟਾਸਕਬਾਰ ਸਿੱਖਿਆ" - "ਇੱਕੋ ਸਮੇਂ \'ਤੇ 2 ਐਪਾਂ ਵਰਤਣ ਲਈ, ਐਪ ਨੂੰ ਪਾਸੇ ਵੱਲ ਘਸੀਟੋ" - "ਟਾਸਕਬਾਰ ਦਿਖਾਉਣ ਲਈ ਥੋੜ੍ਹਾ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" + "ਇੱਕੋ ਵੇਲੇ 2 ਐਪਾਂ ਵਰਤਣ ਲਈ, ਕਿਸੇ ਐਪ ਨੂੰ ਸਾਈਡ \'ਤੇ ਘਸੀਟੋ" + "ਟਾਸਕਬਾਰ ਦਿਖਾਉਣ ਲਈ ਹੌਲੀ ਜਿਹੀ ਉੱਤੇ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" "ਤੁਹਾਡੇ ਨਿਯਮਬੱਧ ਕੰਮ ਦੇ ਆਧਾਰ \'ਤੇ ਐਪ ਸੁਝਾਅ ਪ੍ਰਾਪਤ ਕਰੋ" "ਟਾਸਕਬਾਰ \'ਤੇ ਪਿੰਨ ਕਰਨ ਲਈ ਵਿਭਾਜਕ \'ਤੇ ਦਬਾਈ ਰੱਖੋ" "ਟਾਸਕਬਾਰ ਦਾ ਹੋਰ ਲਾਹਾ ਲਓ" + "ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਉਣਾ" + "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਉਣ ਲਈ, ਵਿਭਾਜਕ ਨੂੰ ਸਪਰਸ਼ ਕਰ ਕੇ ਰੱਖੋ" "ਬੰਦ ਕਰੋ" "ਸਮਝ ਲਿਆ" "ਘਰ" diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml index 7cc04ec30e..439ee353f4 100644 --- a/quickstep/res/values-pl/strings.xml +++ b/quickstep/res/values-pl/strings.xml @@ -49,6 +49,7 @@ "Pamiętaj, aby przesuwać palcem od samej krawędzi (prawej lub lewej)" "Pamiętaj, aby przesuwać palcem od prawej lub lewej krawędzi do środka ekranu i podnieść palec" "Wiesz już, jak przesuwać palcem, aby przejść wstecz. Poćwicz teraz przełączanie aplikacji." + "Gest przejścia wstecz został opanowany. Poćwicz teraz przełączanie aplikacji." "Gest przejścia wstecz został opanowany" "Pamiętaj, aby nie przesuwać palcem zbyt blisko dolnej części ekranu" "Czułość gestu cofania możesz zmienić w Ustawieniach" @@ -95,6 +96,7 @@ "Zrzut ekranu" "Podziel" "Aby podzielić ekran, kliknij drugą aplikację" + "Aby podzielić ekran, wybierz drugą aplikację" "Anuluj" "Wyjdź z wyboru podzielonego ekranu" "Wybierz drugą aplikację, aby podzielić ekran" @@ -106,11 +108,13 @@ "Pomiń" "Obróć ekran" "Informacje o pasku aplikacji" - "Przeciągnij aplikację w bok, aby używać 2 aplikacji naraz" - "Aby wyświetlić pasek aplikacji, przesuń palcem krótko w górę" - "Otrzymuj sugestie aplikacji na podstawie rutyny" + "Przeciągnij aplikację na bok, aby używać 2 aplikacji naraz" + "Aby wyświetlić pasek aplikacji, powoli przesuń palcem w górę" + "Otrzymuj sugestie aplikacji na podstawie typowych działań" "Przytrzymaj separator, aby przypiąć pasek aplikacji" "Wykorzystaj potencjał paska aplikacji" + "Zawsze wyświetlaj pasek aplikacji" + "Aby zawsze wyświetlać pasek aplikacji u dołu ekranu, naciśnij i przytrzymaj linię dzielenia ekranu" "Zamknij" "Gotowe" "Ekran główny" diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml index b90863d246..bacdcf07a7 100644 --- a/quickstep/res/values-pt-rPT/strings.xml +++ b/quickstep/res/values-pt-rPT/strings.xml @@ -36,7 +36,7 @@ "Aceda facilmente às suas apps mais utilizadas, diretamente no ecrã principal. As sugestões mudam em função das suas rotinas. As apps na última fila passam para o ecrã principal." "Aceda facilmente às suas apps mais utilizadas no ecrã principal. As sugestões mudam em função das suas rotinas. As apps na fila dos favoritos passam para o ecrã principal." "Obter sugestões de apps" - "Não, obrigado" + "Não" "Definições" "As apps mais utilizadas aparecem aqui e mudam em função das rotinas." "Arraste as apps para fora da última fila para ver sugestões de apps." @@ -49,6 +49,7 @@ "Deslize rapidamente a partir da extremidade mais à direita ou mais à esquerda" "Deslize rapidamente a partir da extremidade esquerda ou direita até ao centro do ecrã e solte" "Aprendeu a deslizar a partir da direita para retroceder. A seguir, saiba como alternar entre apps." + "Concluiu o gesto para retroceder. A seguir, saiba como alternar entre apps." "Concluiu o gesto para retroceder" "Garanta que não desliza rapidamente com o dedo demasiado perto da parte inferior do ecrã" "Altere a sensibilidade do gesto para voltar nas Definições." @@ -87,7 +88,7 @@ "Tutorial %1$d/%2$d" "Tudo pronto!" "Deslize rapidamente para cima para aceder ao ecrã principal" - "Toque no botão página inicial para aceder ao ecrã principal" + "Toque no botão do ecrã principal para aceder ao ecrã principal" "Já pode começar a usar o seu %1$s" "dispositivo" "Definições de navegação do sistema" @@ -95,6 +96,7 @@ "Fazer captura de ecrã" "Dividir" "Toque noutra app para usar o ecrã dividido" + "Escolha outra app para usar o ecrã dividido" "Cancelar" "Saia da seleção de ecrã dividido" "Escolher outra app para usar o ecrã dividido" @@ -107,10 +109,12 @@ "Rodar ecrã" "Educação da Barra de tarefas" "Arraste uma app para o lado para usar 2 apps em simultâneo" - "Deslize lentamente para cima para mostrar a Barra de tarefas" - "Obtenha sugestões de apps baseadas na sua rotina" + "Deslize lentamente para cima para ver a Barra de tarefas" + "Receba sugestões de apps baseadas na sua rotina" "Mantenha o divisor premido para fixar a Barra de tarefas" "Faça mais com a Barra de tarefas" + "Mostre sempre a Barra de tarefas" + "Para mostrar sempre a Barra de tarefas no fundo do ecrã, toque sem soltar no divisor" "Fechar" "Concluir" "Início" @@ -124,7 +128,7 @@ "Barra de tarefas apresentada" "Barra de tarefas ocultada" "Barra de navegação" - "Mostr. sempre Barra de tarefas" + "Ver sempre Barra de tarefas" "Alterar modo de navegação" "Divisor da Barra de tarefas" "Mover para a parte superior esquerda" diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml index 2e4e2f8ced..2fdf05dd2d 100644 --- a/quickstep/res/values-pt/strings.xml +++ b/quickstep/res/values-pt/strings.xml @@ -49,6 +49,7 @@ "Deslize da borda direita ou esquerda" "Deslize da borda direita ou esquerda até o meio da tela e solte" "Você aprendeu a deslizar da direita para voltar. A seguir, aprenda a trocar de app." + "Você concluiu o gesto para voltar. A seguir, aprenda a trocar de app." "Você concluiu o gesto para voltar" "Não deslize perto demais da parte inferior da tela" "Mude a sensibilidade do gesto de voltar nas configurações" @@ -95,6 +96,7 @@ "Capturar tela" "Dividir" "Toque em outro app para usar a tela dividida" + "Escolha outro app para usar na tela dividida" "Cancelar" "Sair da seleção de tela dividida" "Escolha outro app para usar na tela dividida" @@ -111,6 +113,8 @@ "Receba sugestões de apps com base na sua rotina" "Mantenha o separador pressionado para fixar a Barra de tarefas" "Aproveite ainda mais a Barra de tarefas" + "Sempre mostrar a Barra de tarefas" + "Toque e pressione o divisor para sempre mostrar a Barra de tarefas na parte de baixo da tela" "Fechar" "Concluído" "Início" diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml index 2f40dc4279..9363392566 100644 --- a/quickstep/res/values-ro/strings.xml +++ b/quickstep/res/values-ro/strings.xml @@ -49,6 +49,7 @@ "Glisează dinspre marginea dreaptă îndepărtată sau dinspre marginea stângă îndepărtată" "Glisează dinspre marginea dreaptă sau stângă spre mijlocul ecranului și eliberează" "Ai învățat să revii la ecranul anterior glisând din dreapta. Acum învață să comuți între aplicații." + "Ați finalizat gestul „înapoi”. În continuare, aflați cum să comutați între aplicații." "Ai finalizat gestul „înapoi”" "Nu glisa prea aproape de partea de jos a ecranului" "Schimbă sensibilitatea gestului „Înapoi” accesând Setările" @@ -95,6 +96,7 @@ "Captură de ecran" "Împărțit" "Atinge altă aplicație pentru ecranul împărțit" + "Alege altă aplicație pentru a folosi ecranul împărțit" "Anulează" "Ieși din selecția cu ecran împărțit" "Alege altă aplicație pentru ecranul împărțit" @@ -111,6 +113,8 @@ "Primește sugestii de aplicații în funcție de rutina ta" "Apasă lung pe separator pentru a fixa Bara de activități" "Fă mai multe din Bara de activități" + "Afișează întotdeauna Bara de activități" + "Pentru a afișa mereu Bara de activități în partea de jos a ecranului, atinge lung separatorul" "Închide" "Gata" "Ecran de pornire" @@ -124,7 +128,7 @@ "Bara de activități este afișată" "Bara de activități este ascunsă" "Bară de navigare" - "Afișează întotdeauna bara de activități" + "Afișează mereu bara" "Schimbă modul de navigare" "Separator pentru bara de activități" "Mută în stânga sus" diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml index 0b7a193098..02ebb29ee0 100644 --- a/quickstep/res/values-ru/strings.xml +++ b/quickstep/res/values-ru/strings.xml @@ -49,6 +49,7 @@ "Проведите справа налево или слева направо от самого края экрана." "Проведите от правого или левого края экрана к центру и отпустите палец." "Теперь вы знаете, как вернуться, проведя справа налево. Далее мы расскажем, как переключаться между приложениями." + "Вы выполнили жест для перехода назад. Теперь мы расскажем, как переключаться между приложениями." "Вы выполнили жест для возврата на предыдущий экран." "Проведите пальцем не слишком близко к нижнему краю экрана." "Уровень чувствительности можно изменить в настройках." @@ -95,6 +96,7 @@ "Скриншот" "Разделить" "Для разделения экрана выберите другое приложение." + "Чтобы использовать разделенный экран, выберите другое приложение." "Отмена" "Выйдите из режима разделения экрана." "Выберите другое приложение для разделения экрана." @@ -109,8 +111,10 @@ "Используйте два приложения сразу, перетащив одно в сторону." "Чтобы открыть панель задач, медленно проведите снизу вверх." "Получайте рекомендации, основанные на ваших действиях." - "Закрепите панель задач долгим нажатием на разделитель" + "Закрепите панель задач долгим нажатием на разделитель." "Используйте все возможности панели задач" + "Закрепите панель задач внизу экрана" + "Для этого нажмите на разделитель и удерживайте его." "Закрыть" "Готово" "Главный экран" diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml index 0801a53178..cef1091b6d 100644 --- a/quickstep/res/values-si/strings.xml +++ b/quickstep/res/values-si/strings.xml @@ -49,6 +49,7 @@ "ඔබ ඈත දකුණු හෝ ඈත වම් දාරයේ සිට ස්වයිප් කරන බව සහතික කර ගන්න" "ඔබ දකුණු හෝ වම් දාරයේ සිට තිරයේ මැදට ස්වයිප් කර අත හරින බව සහතික කර ගන්න" "ආපසු යාමට දකුණේ සිට ස්වයිප් කරන්නේ කෙසේදැයි ඔබ දැන ගත්තේය. ඊළඟට, යෙදුම් මාරු කරන ආකාරය දැන ගන්න." + "ඔබ ආපසු යාමේ ඉංගිතය සම්පූර්ණ කරන ලදි. ඊළඟට, යෙදුම් මාරු කරන ආකාරය දැන ගන්න." "ඔබ ආපසු යාමේ ඉංගිතය සම්පූර්ණ කළා" "ඔබ තිරයේ පහළට ඉතාම සමීපව ස්වයිප් නොකරන බවට සහතික කර ගන්න" "ආපසු ඉංගිතයෙහි සංවේදීතාව වෙනස් කිරීමට, සැකසීම් වෙත යන්න" @@ -95,6 +96,7 @@ "තිර රුව" "බෙදන්න" "බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න" + "බෙදුම් තිරය භාවිත කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" "අවලංගු කරන්න" "බෙදීම් තිර තේරීමෙන් පිටවන්න" "බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" @@ -111,6 +113,8 @@ "ඔබේ දිනචරියාව මත පදනම්ව යෙදුම් යෝජනා ලබා ගන්න" "කාර්ය තීරුව ඇමිණීමට බෙදනය මත දිගු වේලාවක් ඔබන්න" "කාර්ය තීරුව සමග තවත් කරන්න" + "සෑම විටම කාර්ය තීරුව පෙන්වන්න" + "සෑම විටම ඔබේ තිරයේ පතුලේ ඇති කාර්ය තීරුව පෙන්වීමට, බෙදුම්කරු ස්පර්ශ කර අල්ලාගෙන සිටින්න" "වසන්න" "නිමයි" "මුල් පිටුව" diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml index 7e1618433c..49acd720ea 100644 --- a/quickstep/res/values-sk/strings.xml +++ b/quickstep/res/values-sk/strings.xml @@ -49,6 +49,7 @@ "Musíte potiahnuť úplne z pravého alebo ľavého okraja" "Musíte potiahnuť z pravého alebo ľavého okraja do stredu obrazovky a potom uvoľniť" "Naučili ste sa prejsť späť potiahnutím sprava. V ďalšom kroku sa naučíte prepínať aplikácie." + "Dokončili ste gesto na prechod späť. V ďalšom kroku sa naučíte, ako prepínať aplikácie." "Dokončili ste gesto na prechod späť" "Nesmiete potiahnuť príliš blízko dolnej časti obrazovky" "Ak chcete zmeniť citlivosť gesta Späť, prejdite do Nastavení" @@ -95,6 +96,7 @@ "Snímka obrazovky" "Rozdeliť" "Obrazovku rozdelíte klepnutím na inú aplikáciu" + "Na použitie rozdelenej obrazovky vyberte ďalšiu aplikáciu" "Zrušiť" "Ukončite výber rozdelenej obrazovky" "Na použitie rozd. obrazovky vyberte inú aplikáciu" @@ -111,6 +113,8 @@ "Získavajte návrhy aplikácií na základe svojich zvykov" "Dlhým stlačením rozdeľovača pripnete panel aplikácií" "Panel aplikácií vám ponúka ďalšie možnosti" + "Vždy zobrazovať panel aplikácií" + "Ak chcete, aby sa panel aplikácií vždy zobrazoval v dolnej časti obrazovky, pridržte rozdeľovač" "Zavrieť" "Hotovo" "Plocha" diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml index 9df20c223e..9f7a92843a 100644 --- a/quickstep/res/values-sl/strings.xml +++ b/quickstep/res/values-sl/strings.xml @@ -49,6 +49,7 @@ "Pazite, da povlečete s skrajno desnega ali skrajno levega roba." "Pazite, da povlečete z desnega ali levega roba do sredine zaslona in dvignete prst." "Naučili ste se, kako povlečete z desne za vrnitev. Zdaj se naučite preklapljanja med aplikacijami." + "Izvedli ste potezo za pomik nazaj. Zdaj se naučite preklapljanja med aplikacijami." "Izvedli ste potezo za pomik nazaj." "Pazite, da ne povlečete preblizu dna zaslona." "Občutljivost poteze za nazaj lahko spremenite v nastavitvah." @@ -95,6 +96,7 @@ "Posnetek zaslona" "Razdeli" "Za razdeljeni zaslon se dotaknite še 1 aplikacije" + "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." "Prekliči" "Zapri izbiro razdeljenega zaslona" "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." @@ -109,8 +111,10 @@ "Povlecite aplikacijo na stran za uporabo 2 aplikacij hkrati." "Počasi povlecite navzgor za prikaz opravilne vrstice" "Prejemajte predloge aplikacij na podlagi svojih navad." - "Pridržite razdelilno črto, da pripnete opravilno vrstico" + "Pridržite razdelilno črto, da pripnete opravilno vrstico." "Naredite več z opravilno vrstico" + "Stalni prikaz opravilne vrstice" + "Če želite, da je opravilna vrstica vedno prikazana na dnu zaslona, pridržite razdelilno črto." "Zapri" "Končano" "Začetni zaslon" @@ -124,7 +128,7 @@ "Opravilna vrstica je prikazana" "Opravilna vrstica je skrita" "Vrstica za krmarjenje" - "Stalen prikaz opravilne vrstice" + "Stalen prikaz oprav. vrstice" "Spreminjanje načina navigacije" "Razdelilnik opravilne vrstice" "Premakni na vrh/levo" diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml index 566a42dffd..bf44975b79 100644 --- a/quickstep/res/values-sq/strings.xml +++ b/quickstep/res/values-sq/strings.xml @@ -49,6 +49,7 @@ "Sigurohu që të rrëshqasësh shpejt nga skaji më i djathtë ose më i majtë" "Sigurohu që të rrëshqasësh shpejt nga skaji i djathtë ose i majtë drejt mesit të ekranit dhe lëshoje" "Ke mësuar si të rrëshqasësh shpejt nga e djathta për t\'u kthyer prapa. Në vijim do të mësosh se si t\'i ndërrosh aplikacionet." + "E ke përfunduar gjestin e kthimit prapa. Në vijim do të mësosh se si t\'i ndërrosh aplikacionet." "E ke përfunduar gjestin e kthimit prapa" "Sigurohu që të mos rrëshqasësh shpejt shumë afër pjesës së poshtme të ekranit" "Për të ndryshuar ndjeshmërinë e gjestit të kthimit prapa, shko te \"Cilësimet\"" @@ -95,6 +96,7 @@ "Pamja e ekranit" "Ndaj" "Trokit një apl. tjetër; përdor ekranin e ndarë" + "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" "Anulo" "Dil nga zgjedhja e ekranit të ndarë" "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" @@ -111,6 +113,8 @@ "Merr sugjerime për aplikacion bazuar në rutinën tënde" "Kryej një shtypje të gjatë te ndarësi për të gozhduar \"Shiritin e detyrave\"" "Bëj më shumë me \"Shiritin e detyrave\"" + "Shfaq gjithmonë \"Shiritin e detyrave\"" + "Prek e mbaj ndarësin dhe shfaq gjithmonë \"Shiritin e detyrave\" në fund të ekranit" "Mbyll" "U krye" "Faqja kryesore" diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml index 78ce2ab4a9..a7bce7b365 100644 --- a/quickstep/res/values-sr/strings.xml +++ b/quickstep/res/values-sr/strings.xml @@ -49,6 +49,7 @@ "Обавезно превуците од саме десне или леве ивице" "Обавезно превуците од десне или леве ивице до средине екрана и отпустите" "Научили сте како да превлачите здесна да бисте се вратили уназад. Сада научите да замените апликације." + "Довршили сте покрет за повратак. Сада сазнајте како да промените апликације." "Довршили сте покрет за повратак" "Никако не превлачите превише близу дна екрана" "Осетљивост пок. за назад можете да промените у Подешавањима" @@ -95,6 +96,7 @@ "Снимак екрана" "Подели" "Додирните другу апликацију за подељени екран" + "Одаберите другу апликацију да бисте користили подељени екран" "Откажи" "Излазак из бирања подељеног екрана" "Одаберите другу апликацију за подељени екран" @@ -107,10 +109,12 @@ "Ротирајте екран" "Упутства на траци задатака" "Превуците на страну да бисте користили 2 апликације одједном" - "Споро превуците нагоре да бисте приказали траку задатака" + "Споро превуците нагоре да бисте видели траку задатака" "Добијајте предлоге апликација на основу рутине" - "Дуго притискајте разделник да бисте закачили траку задатака" + "Дуго притисните разделник да бисте закачили траку задатака" "Урадите више помоћу траке задатака" + "Увек приказуј траку задатака" + "Да би трака задатака увек била приказана у дну екрана, додирните и задржите разделник" "Затвори" "Готово" "Почетна" diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml index a1e2048824..58e5fc4f4d 100644 --- a/quickstep/res/values-sv/strings.xml +++ b/quickstep/res/values-sv/strings.xml @@ -49,6 +49,7 @@ "Se till att du sveper ända från högerkanten eller vänsterkanten" "Se till att du sveper från den högra eller vänstra kanten till mitten av skärmen och sedan släpper" "Nu kan du svepa från höger för att gå tillbaka. Nu ska du få lära dig hur du byter mellan appar." + "Du är klar med rörelsen för att gå tillbaka. Nu ska du få lära dig hur du byter mellan appar." "Du är klar med rörelsen för att gå tillbaka" "Se till att du inte sveper för nära skärmens nederkant" "Öppna inställningarna om du vill ändra rörelsens känslighet" @@ -95,6 +96,7 @@ "Skärmbild" "Delat" "Tryck på en annan app för att använda delad skärm" + "Välj en annan app för att använda delad skärm" "Avbryt" "Avsluta val av delad skärm" "Välj en annan app för att använda delad skärm" @@ -111,6 +113,8 @@ "Få appförslag utifrån dina rutiner" "Tryck länge på avskiljaren om du vill fästa aktivitetsfältet" "Gör mer med aktivitetsfältet" + "Visa alltid aktivitetsfältet" + "Tryck länge på avgränsaren för att alltid visa aktivitetsfältet längst ned på skärmen" "Stäng" "Klar" "Startsida" diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml index 089672cb3d..c5a9df002c 100644 --- a/quickstep/res/values-sw/strings.xml +++ b/quickstep/res/values-sw/strings.xml @@ -49,6 +49,7 @@ "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto kabisa" "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto hadi katikati ya skrini na uachilie" "Umejifunza jinsi ya kutelezesha kidole kuanzia kulia ili kurudi nyuma. Sasa jifunze jinsi ya kubadilisha programu." + "Umekamilisha ishara ya kurudi nyuma. Hatua inayofuata, jifunze jinsi ya kubadilisha programu." "Umeweka ishara ya kurudi nyuma" "Hakikisha hutelezeshi kidole karibu sana na sehemu ya chini ya skrini" "Kubadilisha hisi ya ishara ya nyuma, nenda kwenye Mipangilio" @@ -95,6 +96,7 @@ "Picha ya skrini" "Iliyogawanywa" "Gusa programu nyingine ili utumie kipengele cha kugawa skrini" + "Chagua programu nyingine ili utumie hali ya kugawa skrini" "Ghairi" "Ondoka kwenye hali ya skrini iliyogawanywa" "Chagua programu nyingine ili utumie hali ya kugawa skrini" @@ -111,6 +113,8 @@ "Pata mapendekezo ya programu kulingana na ratiba yako" "Bonyeza kwa muda mrefu kigawaji ili ubandike Upauzana" "Kamilisha mengi kwa kutumia Upauzana huu" + "Onyesha Upauzana kila wakati" + "Ili uonyeshe Upauzana kila wakati chini ya skrini yako, gusa na ushikilie kitenganishi" "Funga" "Imemaliza" "Mwanzo" @@ -124,7 +128,7 @@ "Upauzana umeonyeshwa" "Upauzana umefichwa" "Sehemu ya viungo muhimu" - "Onyesha Upauzana kila wakati" + "Onyesha Zana kila wakati" "Badilisha hali ya usogezaji" "Kitenganishi cha Upauzana" "Sogeza juu/kushoto" diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml index 49ddb8cc2d..a15824c651 100644 --- a/quickstep/res/values-ta/strings.xml +++ b/quickstep/res/values-ta/strings.xml @@ -49,6 +49,7 @@ "வலது அல்லது இடது ஓரத்தின் விளிம்பிலிருந்து ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" "வலது அல்லது இடது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்தபிறகு விடுவிப்பதை உறுதிசெய்க" "பின்செல்வதற்கு எப்படி வலதுபுறத்திலிருந்து ஸ்வைப் செய்வதென்று கற்றுக்கொண்டீர்கள். அடுத்து ஆப்ஸுக்கிடையே எப்படி மாறுவது என்பதை அறிக." + "பின்செல் சைகைப் பயிற்சியை முடித்துவிட்டீர்கள். அடுத்து, ஆப்ஸுக்கிடையே மாறுவது எப்படி என்பதை அறிக." "பின்செல் சைகைப் பயிற்சியை நிறைவுசெய்துவிட்டீர்கள்" "திரையின் கீழ்ப்பகுதிக்கு மிக நெருக்கமாக ஸ்வைப் செய்யவில்லை என்பதை உறுதிசெய்துகொள்ளுங்கள்" "பின்செல் சைகையின் உணர்திறனை மாற்ற அமைப்புகளுக்குச் செல்க" @@ -95,6 +96,7 @@ "ஸ்கிரீன்ஷாட்" "பிரி" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்" + "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தேர்வுசெய்யுங்கள்" "ரத்துசெய்" "திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்" "திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க" @@ -106,11 +108,13 @@ "தவிர்" "திரையைச் சுழற்றும்" "செயல் பட்டியைப் பயன்படுத்தும் விதம்" - "ஒரே நேரத்தில் 2 ஆப்ஸைப் பயன்படுத்தப் பக்கவாட்டில் இழுக்கவும்" + "ஆப்ஸை பக்கவாட்டில் இழுத்து ஒரே நேரத்தில் 2 ஆப்ஸைப் பயன்படுத்தலாம்" "செயல் பட்டியைக் காட்ட மேல்நோக்கி மெதுவாக ஸ்வைப் செய்யவும்" "உங்கள் வழக்கத்திற்கேற்ப ஆப்ஸ் பரிந்துரைகளைப் பெறலாம்" - "பிரிப்பானை நீண்ட நேரம் அழுத்தி, செயல் பட்டியைப் பின் செய்க" + "பிரிப்பானை நீண்ட நேரம் அழுத்தி, செயல் பட்டியைப் பின் செய்யலாம்" "செயல் பட்டி மூலம் மேலும் பலவற்றைச் செய்யுங்கள்" + "செயல் பட்டியை எப்போதும் காட்டுதல்" + "திரையின் கீழ்ப்பகுதியில் செயல் பட்டியை எப்போதும் காட்டுவதற்கு, பிரிப்பானைத் தொட்டுப் பிடித்திருக்கவும்" "மூடுக" "முடிந்தது" "முகப்பு" diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml index d13764eaff..1c422037fc 100644 --- a/quickstep/res/values-te/strings.xml +++ b/quickstep/res/values-te/strings.xml @@ -21,7 +21,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "పిన్ చేయండి" "సంప్రదాయేతర" - "ఇటీవలి అంశాలు ఏవీ లేవు" + "ఇటీవలి ఐటెమ్‌లు ఏవీ లేవు" "యాప్ వినియోగ సెట్టింగ్‌లు" "అన్నీ తీసివేయండి" "ఇటీవలి యాప్‌లు" @@ -49,6 +49,7 @@ "కుడి వైపు చిట్ట చివరి లేదా ఎడమ వైపు చిట్ట చివరి అంచు నుండి స్వైప్ చేస్తున్నారని నిర్ధారించుకోండి" "మీరు కుడి లేదా ఎడమ అంచు నుండి స్క్రీన్ మధ్యలోకి స్వైప్ చేశారని నిర్ధారించుకుని, మీ వేలిని ఎత్తండి" "వెనుకకు వెళ్లడానికి కుడి నుండి స్వైప్ ఎలానో మీకు తెలుసు. తర్వాత, యాప్‌ల మధ్య ఎలా మారాలో తెలుసుకోండి." + "మీరు తిరిగి వెనక్కు వెళ్లే సంజ్ఞను పూర్తి చేశారు. తర్వాత, యాప్‌ల మధ్య ఎలా మారాలో తెలుసుకోండి." "మీరు పేజీ నుండి వెనుకకు వెళ్లే సంజ్ఞను పూర్తి చేశారు" "మీరు స్క్రీన్ దిగువకు చాలా దగ్గరగా స్వైప్ చేయకుండా చూసుకోండి" "వెనుక సంజ్ఞ సున్నితత్వం మార్చడానికి, సెట్టింగ్‌లకు వెళ్లండి" @@ -95,6 +96,7 @@ "స్క్రీన్‌షాట్" "స్ప్లిట్ చేయండి" "స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి" + "స్ప్లిట్ స్క్రీన్‌ను ఉపయోగించడానికి మరొక యాప్ ఎంచుకోండి" "రద్దు చేయండి" "స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి" "స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి" @@ -109,8 +111,10 @@ "ఒకేసారి 2 యాప్‌లను ఉపయోగించడానికి యాప్‌ను పక్కకు లాగండి" "టాస్క్‌బార్‌ను చూపడానికి నెమ్మదిగా పైకి స్వైప్ చేయండి" "మీ రొటీన్ ఆధారంగా యాప్ సూచనలను పొందండి" - "టాస్క్‌బార్‌ను పిన్ చేయడానికి డివైడర్‌పై ఎక్కువసేపు నొక్కి, ఉంచడం" + "టాస్క్‌బార్‌ను పిన్ చేయడానికి డివైడర్‌ను ఎక్కువసేపు నొక్కండి" "టాస్క్‌బార్‌తో మరిన్ని చేయండి" + "టాస్క్‌బార్‌ను నిరంతరం చూపండి" + "మీ స్క్రీన్ దిగువున టాస్క్‌బార్‌ను నిరంతరం చూపడానికి, డివైడర్‌ను తాకి, నొక్కి ఉంచండి" "మూసివేయండి" "పూర్తయింది" "మొదటి ట్యాబ్" @@ -124,7 +128,7 @@ "టాస్క్‌బార్ చూపబడింది" "టాస్క్‌బార్ దాచబడింది" "నావిగేషన్ బార్" - "ఎప్పుడూ టాస్క్‌బార్ చూపించండి" + "టాస్క్‌బార్‌ను నిరంతరం చూపండి" "నావిగేషన్ మోడ్‌ను మార్చండి" "టాస్క్‌బార్ డివైడర్" "ఎగువ/ఎడమ వైపునకు తరలించండి" diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml index 7447b6d432..3b246d6f90 100644 --- a/quickstep/res/values-th/strings.xml +++ b/quickstep/res/values-th/strings.xml @@ -49,6 +49,7 @@ "ตรวจสอบว่าปัดจากขอบด้านขวาสุดหรือซ้ายสุด" "ตรวจสอบว่าปัดจากขอบด้านขวาหรือซ้ายไปตรงกลางหน้าจอ แล้วยกนิ้วขึ้น" "คุณรู้วิธีปัดจากด้านขวาเพื่อย้อนกลับแล้ว ต่อไปดูวิธีสลับแอป" + "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว ต่อไปดูวิธีสลับแอป" "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว" "ไม่ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป" "เปลี่ยนความไวของท่าทางสัมผัสเพื่อย้อนกลับได้ที่การตั้งค่า" @@ -95,6 +96,7 @@ "ภาพหน้าจอ" "แยก" "แตะแอปอื่นเพื่อใช้การแยกหน้าจอ" + "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" "ยกเลิก" "ออกจากการเลือกโหมดแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" @@ -111,6 +113,8 @@ "รับคำแนะนำเกี่ยวกับแอปตามกิจวัตรของคุณ" "กดตัวแบ่งค้างไว้เพื่อปักหมุดแถบงาน" "ทำสิ่งต่างๆ ได้มากขึ้นด้วยแถบงาน" + "แสดงแถบงานเสมอ" + "หากต้องการให้แถบงานแสดงที่ด้านล่างหน้าจออยู่เสมอ ให้แตะตัวแบ่งค้างไว้" "ปิด" "เสร็จ" "หน้าแรก" diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml index cd432cc73a..08e030d3a0 100644 --- a/quickstep/res/values-tl/strings.xml +++ b/quickstep/res/values-tl/strings.xml @@ -49,6 +49,7 @@ "Tiyaking magsa-swipe ka mula sa dulong kanan o dulong kaliwang gilid" "Tiyaking magsa-swipe mula sa kanan o kaliwang gilid papunta sa gitna ng screen at iangat ang daliri" "Natuto kang mag-swipe mula sa kanan para bumalik. Sunod, alamin kung paano magpalipat-lipat ng app." + "Nakumpleto mo na ang galaw para bumalik. Susunod, alamin kung paano magpalipat-lipat sa mga app." "Nakumpleto mo na ang galaw para bumalik" "Tiyaking hindi ka magsa-swipe nang masyadong malapit sa ibaba ng screen" "Pumunta sa Settings para baguhin ang sensitivity ng pagbalik" @@ -95,6 +96,7 @@ "Screenshot" "Split" "Mag-tap ng ibang app para gamitin ang split screen" + "Pumili ng ibang app para gamitin ang split screen" "Kanselahin" "Lumabas sa pagpili ng split screen" "Pumili ng ibang app para gamitin ang split screen" @@ -111,6 +113,8 @@ "Makakuha ng mga iminumungkahing app batay sa iyong routine" "Pumindot nang matagal sa divider para i-pin ang Taskbar" "Mas maraming magawa gamit ang Taskbar" + "Palaging ipakita ang Taskbar" + "Para palaging ipakita ang Taskbar sa ibaba ng iyong screen, pindutin nang matagal ang divider" "Isara" "Tapos na" "Home" @@ -124,7 +128,7 @@ "Ipinapakita ang taskbar" "Nakatago ang taskbar" "Navigation bar" - "Palaging ipakita ang Taskbar" + "Ipakita lagi ang Taskbar" "Magpalit ng navigation mode" "Divider ng Taskbar" "Ilipat sa itaas/kaliwa" diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml index ed22286a9c..cedfa2fd93 100644 --- a/quickstep/res/values-tr/strings.xml +++ b/quickstep/res/values-tr/strings.xml @@ -49,6 +49,7 @@ "En sağ veya en sol kenardan kaydırdığınızdan emin olun" "Ekranın sağ veya sol kenarından ortasına doğru sürükleyip bıraktığınızdan emin olun." "Geri dönmek için sağdan kaydırmayı öğrendiniz. Sırada uygulamalar arasında geçiş yapma var." + "Geri dön hareketini tamamladınız. Sırada, uygulamalar arasında geçiş yapmayı öğrenmek var." "Geri dön hareketini tamamladınız" "Ekranın alt kısmına çok yakın bir şekilde kaydırmadığınızdan emin olun" "Geri hareketinin hassasiyetini değiştirmek için Ayarlar\'a gidin" @@ -95,6 +96,7 @@ "Ekran görüntüsü" "Böl" "Bölünmüş ekran için başka bir uygulamaya dokunun" + "Bölünmüş ekran kullanmak için başka bir uygulama seçin" "İptal" "Bölünmüş ekran seçiminden çıkın" "Bölünmüş ekran kullanmak için başka bir uygulama seçin" @@ -107,10 +109,12 @@ "Ekranı döndür" "Görev çubuğu eğitimi" "Aynı anda iki uygulama kullanmak için birini yana sürükleyin" - "Görev çubuğunu göstermek için yukarı doğru yavaşça kaydırın" + "Görev çubuğunun görünmesi için yukarı doğru yavaşça kaydırın" "Rutininize göre uygulama önerileri alın" "Görev çubuğunu sabitlemek için ayırıcıya uzun basın" "Görev çubuğuyla daha fazla şey yapın" + "Görev çubuğunu sabitleyin" + "Ayırıcıya dokunup basılı tuttuğunuzda görev çubuğu ekranın alt kısmına sabitlenir" "Kapat" "Bitti" "Ana ekran" diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml index 42fc6ddfee..71257c8fbc 100644 --- a/quickstep/res/values-uk/strings.xml +++ b/quickstep/res/values-uk/strings.xml @@ -49,6 +49,7 @@ "Проведіть пальцем від самого краю екрана (правого або лівого)" "Проведіть пальцем від правого або лівого краю до середини екрана й підніміть палець" "Тепер ви знаєте, як повернутися на попередній екран, провівши пальцем справа наліво. Дізнайтеся, як переключатися між додатками." + "Ви виконали жест \"Назад\". Тепер дізнайтеся, як переходити між додатками." "Ви виконали жест \"Назад\"" "Не проводьте пальцем надто близько до нижнього краю екрана" "Щоб змінити чутливість жесту \"Назад\", відкрийте налаштування" @@ -94,10 +95,11 @@ "Поділитися" "Знімок екрана" "Розділити" - "Щоб розділити екран, виберіть ще один додаток" + "Щоб розділити екран, виберіть ще один додаток." + "Щоб розділити екран, виберіть ще один додаток." "Скасувати" "Вийти з режиму розділення екрана" - "Щоб розділити екран, виберіть ще один додаток" + "Щоб розділити екран, виберіть ще один додаток." "Ця дія заборонена додатком або адміністратором організації" "Віджети наразі не підтримуються. Виберіть інший додаток." "Пропустити посібник із навігації?" @@ -111,6 +113,8 @@ "Отримуйте рекомендації додатків залежно від їх використання" "Утримуйте розділювач, щоб закріпити панель завдань" "Більше можливостей завдяки панелі завдань" + "Завжди показувати панель завдань" + "Щоб завжди показувати панель завдань унизу екрана, натисніть і втримуйте роздільник" "Закрити" "Готово" "Головний екран" diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml index 0e5b60255b..9beaf07e15 100644 --- a/quickstep/res/values-ur/strings.xml +++ b/quickstep/res/values-ur/strings.xml @@ -49,6 +49,7 @@ "یقینی بنائیں کہ آپ دائیں یا بائیں کنارے سے دور سے سوائپ کریں" "یقینی بنائیں کہ آپ دائیں یا بائیں کنارے سے اسکرین کے وسط تک سوائپ کریں اور پھر اپنی انگلی اٹھا لیں" "آپ نے واپس جانے کے لیے دائیں کنارے سے سوائپ کرنے کا طریقہ سیکھ لیا۔ اس کے بعد ایپس سوئچ کرنے کا طریقہ جانیں۔" + "آپ نے واپس جائیں اشارے کو مکمل کر لیا۔ اس کے بعد ایپس سوئچ کرنے کا طریقہ جانیں۔" "آپ نے واپس جائیں اشارے کو مکمل کر لیا" "اس بات کو یقینی بنائیں کہ آپ اسکرین کے نچلے حصے سے زیادہ قریب سے سوائپ نہ کریں" "پچھلے اشارے کی حساسیت تبدیل کرنے کے لیے ترتیبات پر جائیں" @@ -95,6 +96,7 @@ "اسکرین شاٹ" "اسپلٹ" "اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں" + "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" "منسوخ کریں" "اسپلٹ اسکرین کے انتخاب سے باہر نکلیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" @@ -111,6 +113,8 @@ "اپنی روٹین پر مبنی ایپس کی تجاویز حاصل کریں" "ٹاسک بار کو پن کرنے کے لیے ڈیوائیڈر پر لانگ پریس کریں" "ٹاسک بار سے بہت کچھ کریں" + "ہمیشہ ٹاسک بار دکھائیں" + "ٹاسک بار کو ہمیشہ اپنی اسکرین کے نیچے دکھانے کے لیے، ڈیوائیڈر کو ٹچ کریں اور دبائے رکھیں" "بند کریں" "ہو گیا" "ہوم" diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml index 7cc157b159..d2f434fe1e 100644 --- a/quickstep/res/values-uz/strings.xml +++ b/quickstep/res/values-uz/strings.xml @@ -49,6 +49,7 @@ "Ekran chetidan boshlab oʻngdan yoki chapdan suring" "Ekranning oʻng yoki chap chetidan oʻrtasigacha suring va qoʻyib yuboring" "Ortga qaytish uchun oʻngdan surishni oʻrgandingiz. Endi ilovalarni almashtirishni oʻrganamiz." + "Ortga qaytish ishorasi darsini tamomladingiz. Endi ilovalarni almashtirishni oʻrganamiz." "Ortga qaytish ishorasi darsini tamomladingiz" "Barmoqni ekran pastiga yaqin surmaslikka harakat qiling" "Orqaga ishorasi sezuvchanligi Sozlamalardan oʻzgartiriladi" @@ -95,6 +96,7 @@ "Skrinshot" "Ajratish" "Ekranni ikkiga ajratish uchun boshqa ilovani bosing" + "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" "Bekor qilish" "Ekranni ikkiga ajratish tanlovidan chiqish" "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" @@ -111,6 +113,8 @@ "Harakatlaringiz asosida tavsiyalar oling." "Vazifa panelini mahkamlash uchun ajratgichni bosib turing" "Vazifalar panelidan maksimal darajada foydalaning" + "Vazifalar paneli doim chiqarilsin" + "Vazifalar panelini ekranning pastki qismida doim chiqib turishi uchun ajratkichni bosib turing" "Yopish" "Tayyor" "Bosh ekran" @@ -124,7 +128,7 @@ "Vazifalar paneli ochiq" "Vazifalar paneli yopiq" "Navigatsiya paneli" - "Doim vazifalar paneli chiqsin" + "Vazifalar paneli doim chiqarilsin" "Navigatsiya rejimini oʻzgartirish" "Vazifalar panelini ajratkich" "Yuqoriga yoki chapga oʻtkazish" diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml index bc9b348978..626c569ba5 100644 --- a/quickstep/res/values-vi/strings.xml +++ b/quickstep/res/values-vi/strings.xml @@ -49,6 +49,7 @@ "Hãy vuốt từ mép ngoài cùng bên phải hoặc ngoài cùng bên trái" "Hãy vuốt từ mép phải hoặc mép trái tới giữa màn hình rồi nhấc ngón tay ra" "Bạn đã học được cách vuốt từ mép phải để quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." + "Bạn đã thực hiện xong cử chỉ quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." "Bạn đã thực hiện xong cử chỉ quay lại" "Hãy nhớ không được vuốt quá gần phần dưới cùng của màn hình" "Để thay đổi độ nhạy của cử chỉ quay lại, hãy vào mục Cài đặt" @@ -95,6 +96,7 @@ "Chụp ảnh màn hình" "Chia đôi màn hình" "Nhấn vào ứng dụng khác để chia đôi màn hình" + "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" "Huỷ" "Thoát khỏi lựa chọn chia đôi màn hình" "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" @@ -109,8 +111,10 @@ "Kéo một ứng dụng sang bên để dùng 2 ứng dụng cùng lúc" "Từ từ vuốt lên để Thanh tác vụ xuất hiện" "Nhận ứng dụng đề xuất dựa trên thói quen của bạn" - "Nhấn và giữ trên đường phân chia để ghim Taskbar" + "Nhấn và giữ trên đường phân chia để ghim Thanh tác vụ" "Làm nhiều việc hơn qua Thanh tác vụ" + "Luôn hiện Taskbar" + "Để luôn hiện Taskbar ở cuối màn hình, hãy nhấn và giữ đường phân chia" "Đóng" "Xong" "Màn hình chính" @@ -124,7 +128,7 @@ "Đã hiện thanh thao tác" "Đã ẩn thanh thao tác" "Thanh điều hướng" - "Luôn hiển thị Taskbar" + "Luôn hiện Thanh tác vụ" "Thay đổi chế độ điều hướng" "Đường phân chia Taskbar" "Chuyển lên trên cùng/sang bên trái" diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml index 2c740b7e7f..f542c9eca2 100644 --- a/quickstep/res/values-zh-rCN/strings.xml +++ b/quickstep/res/values-zh-rCN/strings.xml @@ -49,6 +49,7 @@ "确保从最右侧或最左侧边缘开始滑动" "确保从右侧或左侧边缘滑动到屏幕中间位置后再松开手指" "您已了解如何使用“从右侧向左滑动”手势返回。接下来学习切换应用吧!" + "您完成了“返回”手势教程。接下来了解如何切换应用。" "您完成了“返回”手势" "确保滑动时手的位置不要太靠近屏幕底部" "如要调节“返回”手势的灵敏度,请转到“设置”" @@ -95,6 +96,7 @@ "屏幕截图" "拆分" "点按另一个应用即可使用分屏" + "另外选择一个应用才可使用分屏模式" "取消" "退出分屏选择模式" "另外选择一个应用才可使用分屏模式" @@ -106,15 +108,17 @@ "跳过" "旋转屏幕" "任务栏教程" - "将一个应用拖到一侧,即可一次使用两个应用" - "缓慢向上滑动即可显示任务栏" - "根据您的日常安排获取应用建议" + "将一个应用拖到一侧,即可同时使用两个应用" + "缓慢上滑即可显示任务栏" + "根据您的日常使用习惯获得应用建议" "长按分隔线即可固定任务栏" "体验任务栏的更多功能" + "始终显示任务栏" + "若要始终在屏幕底部显示任务栏,请轻触并按住分隔线" "关闭" "完成" "主屏幕" - "无障碍" + "无障碍功能" "返回" "IME 切换器" "最近用过" diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml index af7c663d80..d01211dbb0 100644 --- a/quickstep/res/values-zh-rHK/strings.xml +++ b/quickstep/res/values-zh-rHK/strings.xml @@ -49,12 +49,13 @@ "請確保從螢幕最右側或最左側邊緣滑動" "請確保從螢幕右側或左側邊緣往中央滑動,然後放開手指" "你已瞭解如何透過「由右向左滑動」手勢返回。接下來一起瞭解如何切換應用程式。" + "你已完成「返回」手勢的教學課程。接下來一起瞭解如何切換應用程式。" "你已完成「返回」手勢的教學課程" "滑動時,手的位置不要太接近螢幕底部" "如要變更「返回」手勢的敏感度,請前往「設定」" "滑動即可返回" "如要返回上一個畫面,請從螢幕左側或右側邊緣往中央滑動。" - "如要返回上一個畫面,請用 2 隻手指從螢幕左側或右側邊緣往中央滑動。" + "如要返回上一個畫面,請用兩指從螢幕左側或右側邊緣往中央滑動。" "返回" "從螢幕左側或右側邊緣往中央滑動" "請確保從螢幕底部邊緣向上滑動" @@ -64,7 +65,7 @@ "你已完成「返回主畫面」手勢的教學課程" "向上滑動即可返回主畫面" "從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" - "請用 2 隻手指從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" + "請用兩指從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" "返回主畫面" "從螢幕底部向上滑動" "太好了!" @@ -75,7 +76,7 @@ "你已完成「切換應用程式」手勢的教學課程" "滑動即可切換應用程式" "如要切換應用程式,請從螢幕底部向上滑動並按住,然後放開。" - "如要切換應用程式,請用 2 隻手指從螢幕底部向上滑動並按住,然後放開手指。" + "如要切換應用程式,請用兩指從螢幕底部向上滑動並按住,然後放開手指。" "切換應用程式" "從螢幕底部向上滑動並按住,然後放開" "做得好!" @@ -95,6 +96,7 @@ "螢幕截圖" "分割" "輕按其他應用程式以使用分割螢幕" + "選擇其他應用程式才能使用分割螢幕" "取消" "退出分割螢幕選取頁面" "選擇其他應用程式才能使用分割螢幕" @@ -106,11 +108,13 @@ "略過" "旋轉螢幕" "工作列教學" - "將應用程式拖曳到一邊,即可同時使用 2 個應用程式" - "慢慢向上滑動即可顯示工作列" - "根據你的日常安排提供應用程式建議" + "將應用程式拖曳到一邊,同時使用兩個應用程式" + "慢慢向上掃即可顯示工作列" + "獲取符合日常習慣的應用程式建議" "長按分隔線即可固定工作列" "工作列助你事半功倍" + "一律顯示工作列" + "如要持續在畫面底部顯示工作列,請按住分隔線" "關閉" "完成" "住宅" diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml index e4ca3d913c..142f8832cb 100644 --- a/quickstep/res/values-zh-rTW/strings.xml +++ b/quickstep/res/values-zh-rTW/strings.xml @@ -49,6 +49,7 @@ "請務必從螢幕最右側或最左側滑動" "請務必從螢幕右側或左側往中央滑動,然後放開手指" "你已瞭解如何透過「由右向左滑動」手勢返回。接著,一起來瞭解如何切換應用程式。" + "你已完成「返回」手勢的教學課程。接著,一起來瞭解如何切換應用程式。" "你已完成「返回」手勢的教學課程" "滑動時,手的位置不要太接近螢幕底部" "如要變更「返回」手勢的敏感度,請前往「設定」" @@ -95,6 +96,7 @@ "螢幕截圖" "分割" "輕觸另一個應用程式即可使用分割畫面" + "選擇要在分割畫面中使用的另一個應用程式" "取消" "退出分割畫面選擇器" "必須選擇另一個應用程式才能使用分割畫面" @@ -107,10 +109,12 @@ "旋轉螢幕" "工作列教學課程" "將應用程式拖曳到一邊即可同時使用 2 個應用程式" - "緩慢向上滑動即可讓工作列顯示在畫面上" + "緩慢向上滑動讓工作列顯示在畫面上" "根據你的日常安排建議應用程式" "長按分隔線即可固定工作列" "充分發揮工作列的功用" + "一律顯示工作列" + "如要一律在畫面底部顯示工作列,請按住分隔線" "關閉" "完成" "主畫面" @@ -129,7 +133,7 @@ "工作列分隔線" "移到上方/左側" "移到底部/右側" - "{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}" + "{count,plural, =1{再多顯示 # 個應用程式。}other{再多顯示 # 個應用程式。}}" "「%1$s」和「%2$s」" "新增應用程式至桌面" "取消" diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml index 0526940f26..bf5a1b7b01 100644 --- a/quickstep/res/values-zu/strings.xml +++ b/quickstep/res/values-zu/strings.xml @@ -49,6 +49,7 @@ "Qinisekisa ukuthi uswayipha ukusuka onqenqemeni olukude ngakwesokudla noma olukude ngakwesokunxele" "Qinisekisa ukuthi uswayipha ukusuka kunqenqema olungakwesokudla noma olungakwesokunxele ukuya maphakathi nesikrini bese uyadedela." "Ufunde indlela yokuswayipha kusuka kwesokudla ukuze ubuyele emuva. Ngokulandelayo, funda indlela yokushintsha ama-app." + "Ukuqedile ukuthinta kokubuyela emuva. Ngokulandelayo, funda indlela yokushintsha ama-app." "Ukuqedile ukuthinta kokubuyela emuva" "Qinisekisa ukuba awuswayipheli eduze kakhulu naphansi kwesikrini" "Ukuze ushintshe ukuzwela kokuthinta emuva, iya Kumasethingi" @@ -95,6 +96,7 @@ "Isithombe-skrini" "Hlukanisa" "Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa" + "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" "Khansela" "Phuma ekukhetheni ukuhlukaniswa kwesikrini" "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" @@ -111,6 +113,8 @@ "Thola iziphakamiso ze-app ngokusekelwe kumjikelezo wakho" "Cindezela isikhathi eside kusihlukanisi ukuze uphine i-Taskbar" "Yenza okwengeziwe nge-Taskbar" + "Bonisa njalo i-Taskbar" + "Ukuze ubonise njalo i-Taskbar phansi kwesikrini sakho, thinta bese ubamba isihlukanisi" "Vala" "Kwenziwe" "Ikhaya" diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index 8a3ffb5a90..e45d9fd095 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -53,5 +53,5 @@ - 48.4 + 44 diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 232c4416a6..f90d96f3f5 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -32,8 +32,11 @@ 50dp - + 0.7 + + 0.46 48dp @@ -44,48 +47,38 @@ 16dp 16dp - - 156dp - - 216dp - - 120dp - - 86dp - - 118dp + + 86dp + + 118dp 14sp - - 164dp - - 36dp - - 28dp - - 36dp - - 52dp - - 24dp - - 16dp - - 12dp - - 12dp + + 156dp + + 216dp + + 36dp + + 52dp + + 4dp + + 8dp + + 8dp - 6dp + 2dp 6dp 8dp - - 8dp - - 24dp - - 32dp + + 24dp + + 24dp + + 32dp 44dp 4dp @@ -322,7 +315,6 @@ @*android:dimen/taskbar_frame_height - @*android:dimen/navigation_bar_frame_height 48dp 48dp @@ -401,6 +393,7 @@ 170dp 106dp 24dp + 412dp 428dp 624dp @@ -408,6 +401,9 @@ 300dp 16dp + + 60dp + 30dp @@ -443,8 +439,7 @@ 4dp 104dp 134dp - 28dp - 4dp + 52dp 20dp 56dp 16dp diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml index 29779a711e..cba1f5bcd0 100644 --- a/quickstep/res/values/override.xml +++ b/quickstep/res/values/override.xml @@ -33,6 +33,8 @@ com.android.launcher3.taskbar.TaskbarModelCallbacksFactory + com.android.launcher3.taskbar.TaskbarViewCallbacksFactory + com.android.quickstep.LauncherRestoreEventLoggerImpl diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 6912e1aec7..14f615e240 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -105,6 +105,8 @@ Make sure you swipe from the right or left edge to the middle of the screen and let go You learned how to swipe from the right to go back. Next up, learn how to switch apps. + + You completed the go back gesture. Next up, learn how to switch apps. You completed the go back gesture @@ -230,6 +232,7 @@ Split Tap another app to use split screen + Choose another app to use split screen Cancel Exit split screen selection @@ -264,6 +267,10 @@ Long press on the divider to pin the Taskbar Do more with the Taskbar + + Always show the Taskbar + + To always show the Taskbar on the bottom of your screen, touch & hold the divider Close diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index bdc86b217d..350c752c56 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -273,7 +273,7 @@ diff --git a/res/xml/split_configuration.xml b/res/xml/split_configuration.xml new file mode 100644 index 0000000000..531fef893b --- /dev/null +++ b/res/xml/split_configuration.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index f72c55615c..e7b88dc9b9 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -124,7 +124,8 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch /** Type of popups that should get exclusive accessibility focus. */ public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER - & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP; + & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP + & ~TYPE_WIDGET_RESIZE_FRAME; // These view all have particular operation associated with swipe down interaction. public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET | diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java index e4aebf606d..fb8088c13b 100644 --- a/src/com/android/launcher3/Alarm.java +++ b/src/com/android/launcher3/Alarm.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; public class Alarm implements Runnable{ @@ -33,7 +34,11 @@ public class Alarm implements Runnable{ private long mLastSetTimeout; public Alarm() { - mHandler = new Handler(); + this(Looper.myLooper()); + } + + public Alarm(Looper looper) { + mHandler = new Handler(looper); } public void setOnAlarmListener(OnAlarmListener alarmListener) { diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index 79b831e117..4a277f0ed5 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -191,14 +191,12 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - if (Utilities.ATLEAST_Q) { - for (int i = 0; i < HANDLE_COUNT; i++) { - View dragHandle = mDragHandles[i]; - mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(), - dragHandle.getRight(), dragHandle.getBottom()); - } - setSystemGestureExclusionRects(mSystemGestureExclusionRects); + for (int i = 0; i < HANDLE_COUNT; i++) { + View dragHandle = mDragHandles[i]; + mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(), + dragHandle.getRight(), dragHandle.getBottom()); } + setSystemGestureExclusionRects(mSystemGestureExclusionRects); } public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) { diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java index f8ed4df453..1c2ed4343d 100644 --- a/src/com/android/launcher3/BaseDraggingActivity.java +++ b/src/com/android/launcher3/BaseDraggingActivity.java @@ -20,11 +20,8 @@ import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Point; -import android.graphics.Rect; import android.os.Bundle; import android.view.ActionMode; -import android.view.Display; import android.view.View; import androidx.annotation.MainThread; @@ -39,7 +36,6 @@ import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.OnColorHintListener; import com.android.launcher3.util.Themes; -import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.WallpaperColorHints; import com.android.launcher3.util.WindowBounds; @@ -55,16 +51,12 @@ public abstract class BaseDraggingActivity extends BaseActivity public static final Object AUTO_CANCEL_ACTION_MODE = new Object(); private ActionMode mCurrentActionMode; - protected boolean mIsSafeModeEnabled; private int mThemeRes = R.style.AppTheme; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode", - () -> getPackageManager().isSafeMode()); DisplayController.INSTANCE.get(this).addChangeListener(this); // Update theme @@ -170,19 +162,11 @@ public abstract class BaseDraggingActivity extends BaseActivity protected abstract void reapplyUi(); protected WindowBounds getMultiWindowDisplaySize() { - if (Utilities.ATLEAST_R) { - return WindowBounds.fromWindowMetrics(getWindowManager().getCurrentWindowMetrics()); - } - // Note: Calls to getSize() can't rely on our cached DefaultDisplay since it can return - // the app window size - Display display = getWindowManager().getDefaultDisplay(); - Point mwSize = new Point(); - display.getSize(mwSize); - return new WindowBounds(new Rect(0, 0, mwSize.x, mwSize.y), new Rect()); + return WindowBounds.fromWindowMetrics(getWindowManager().getCurrentWindowMetrics()); } @Override public boolean isAppBlockedForSafeMode() { - return mIsSafeModeEnabled; + return LauncherAppState.getInstance(this).isSafeModeEnabled(); } } diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 91da7e6404..3ae1ce117b 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -19,9 +19,9 @@ package com.android.launcher3; import static android.text.Layout.Alignment.ALIGN_NORMAL; import static com.android.launcher3.Flags.enableCursorHoverStates; -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_SKIP_USER_BADGE; import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE; @@ -69,13 +69,13 @@ import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.icons.PlaceHolderIconDrawable; -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.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.search.StringMatcherUtility; +import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.SafeCloseable; @@ -164,6 +164,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @ViewDebug.ExportedProperty(category = "launcher") private boolean mHideBadge = false; @ViewDebug.ExportedProperty(category = "launcher") + private boolean mSkipUserBadge = false; + @ViewDebug.ExportedProperty(category = "launcher") private boolean mIsIconVisible = true; @ViewDebug.ExportedProperty(category = "launcher") private int mTextColor; @@ -188,7 +190,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @ViewDebug.ExportedProperty(category = "launcher") private boolean mDisableRelayout = false; - private HandlerRunnable mIconLoadRequest; + private CancellableTask mIconLoadRequest; private boolean mEnableIconUpdateAnimation = false; @@ -211,6 +213,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mIsRtl = (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); mDeviceProfile = mActivity.getDeviceProfile(); + mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); final int defaultIconSize; @@ -241,7 +244,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, defaultIconSize = mDeviceProfile.iconSizePx; } - mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, defaultIconSize); @@ -268,6 +270,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mHideBadge = hideBadge; } + public void setSkipUserBadge(boolean skipUserBadge) { + mSkipUserBadge = skipUserBadge; + } + /** * Resets the view so it can be recycled. */ @@ -279,15 +285,19 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mDotParams.scale = 0f; mForceHideDot = false; setBackground(null); - if (FeatureFlags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()) { - setMaxLines(1); - } setTag(null); if (mIconLoadRequest != null) { mIconLoadRequest.cancel(); mIconLoadRequest = null; } + // Reset any shifty arrangements in case animation is disrupted. + setPivotY(0); + setAlpha(1); + setScaleY(1); + setTranslationY(0); + setMaxLines(1); + setVisibility(VISIBLE); } private void cancelDotScaleAnim() { @@ -397,6 +407,9 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) { flags |= FLAG_NO_BADGE; } + if (mSkipUserBadge) { + flags |= FLAG_SKIP_USER_BADGE; + } FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags); mDotParams.appColor = iconDrawable.getIconColor(); mDotParams.dotColor = Themes.getAttrColor(getContext(), R.attr.notificationDotColor); @@ -410,11 +423,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } /** - * Only if actual text can be displayed in two line, the {@code true} value will be effective. + * Only if actual text can be displayed in two line, the {@code true} value will be effective. */ protected boolean shouldUseTwoLine() { - return (FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish()) - && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW); + return isCurrentLanguageEnglish() && (mDisplay == DISPLAY_ALL_APPS + || mDisplay == DISPLAY_PREDICTION_ROW) && (Flags.enableTwolineToggle() + && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())); } protected boolean isCurrentLanguageEnglish() { @@ -546,9 +560,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } private void checkForEllipsis() { - if (!ENABLE_ICON_LABEL_AUTO_SCALING.get()) { - return; - } float width = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); if (width <= 0) { return; @@ -569,12 +580,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, /** * Find the appropriate text spacing to display the provided text - * @param paint the paint used by the text view - * @param text the text to display - * @param allowedWidthPx available space to render the text - * @param minSpacingEm minimum spacing allowed between characters - * @return the final textSpacing value * + * @param paint the paint used by the text view + * @param text the text to display + * @param allowedWidthPx available space to render the text + * @param minSpacingEm minimum spacing allowed between characters + * @return the final textSpacing value * @see #setLetterSpacing(float) */ private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx, @@ -802,13 +813,13 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, * iterating through the list of break points and determining if the strings between the break * points can fit within the line it is in. We will show the modified string if there is enough * horizontal and vertical space, otherwise this method will just return the original string. - * Example assuming each character takes up one spot: - * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7 - * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery, - * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth - * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking - * if the first char is a SPACE, we trim to append "Stats". So resulting string would be - * "Battery\nStats" + * Example assuming each character takes up one spot: + * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7 + * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery, + * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth + * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking + * if the first char is a SPACE, we trim to append "Stats". So resulting string would be + * "Battery\nStats" */ public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight, CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier, @@ -820,28 +831,27 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, float currentWordWidth, runningWidth = 0; CharSequence currentWord; StringBuilder newString = new StringBuilder(); - // TODO: Remove when ENABLE_ICON_LABEL_AUTO_SCALING feature flag is being cleaned up. paint.setLetterSpacing(MIN_LETTER_SPACING); int stringPtr = 0; - for (int i = 0; i < breakPoints.size()+1; i++) { + for (int i = 0; i < breakPoints.size() + 1; i++) { if (i < breakPoints.size()) { - currentWord = title.subSequence(stringPtr, breakPoints.get(i)+1); + currentWord = title.subSequence(stringPtr, breakPoints.get(i) + 1); } else { // last word from recent breakpoint until the end of the string currentWord = title.subSequence(stringPtr, title.length()); } - currentWordWidth = paint.measureText(currentWord,0, currentWord.length()); + currentWordWidth = paint.measureText(currentWord, 0, currentWord.length()); runningWidth += currentWordWidth; if (runningWidth <= limitedWidth) { newString.append(currentWord); } else { - if (i != 0) { + if (i != 0) { // If putting word onto a new line, make sure there is no space or new line // character in the beginning of the current word and just put in the rest of // the characters. CharSequence lastCharacters = title.subSequence(stringPtr, title.length()); int beginningLetterType = - Character.getType(Character.codePointAt(lastCharacters,0)); + Character.getType(Character.codePointAt(lastCharacters, 0)); if (beginningLetterType == Character.SPACE_SEPARATOR || beginningLetterType == Character.LINE_SEPARATOR) { lastCharacters = lastCharacters.length() > 1 @@ -862,7 +872,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, // no need to look forward into the string if we've already finished processing break; } - stringPtr = breakPoints.get(i)+1; + stringPtr = breakPoints.get(i) + 1; } return newString.toString(); } @@ -923,7 +933,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (mIcon instanceof PreloadIconDrawable) { preloadIconDrawable = (PreloadIconDrawable) mIcon; preloadIconDrawable.setLevel(progressLevel); - preloadIconDrawable.setIsDisabled(info.getProgressLevel() == 0); + preloadIconDrawable.setIsDisabled(isIconDisabled(info)); } else { preloadIconDrawable = makePreloadIcon(); setIcon(preloadIconDrawable); @@ -948,10 +958,18 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info); preloadDrawable.setLevel(progressLevel); - preloadDrawable.setIsDisabled(info.getProgressLevel() == 0); + preloadDrawable.setIsDisabled(isIconDisabled(info)); return preloadDrawable; } + /** + * Returns true to grey the icon if the icon is either suspended or if the icon is pending + * download + */ + public boolean isIconDisabled(ItemInfoWithIcon info) { + return info.isDisabled() || info.isPendingDownload(); + } + public void applyDotState(ItemInfo itemInfo, boolean animate) { if (mIcon instanceof FastBitmapDrawable) { boolean wasDotted = mDotInfo != null; @@ -989,19 +1007,21 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) { - if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) + if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0 && progressLevel == 0) { + setContentDescription(getContext().getString(R.string.app_archived_title, info.title)); + } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { String percentageString = NumberFormat.getPercentInstance() .format(progressLevel * 0.01); if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) { setContentDescription(getContext() .getString( - R.string.app_installing_title, info.title, percentageString)); + R.string.app_installing_title, info.title, percentageString)); } else if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) { setContentDescription(getContext() .getString( - R.string.app_downloading_title, info.title, percentageString)); + R.string.app_downloading_title, info.title, percentageString)); } } } @@ -1168,7 +1188,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, public SafeCloseable prepareDrawDragView() { resetIconScale(); setForceHideDot(true); - return () -> { }; + return () -> { + }; } private void resetIconScale() { diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 5443ff992d..72758f2e6c 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -16,18 +16,12 @@ package com.android.launcher3; -import static android.animation.ValueAnimator.areAnimatorsEnabled; - -import static com.android.app.animation.Interpolators.DECELERATE_1_5; -import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON; import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; -import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; @@ -48,7 +42,6 @@ import android.util.ArrayMap; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; -import android.util.Property; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; @@ -71,6 +64,7 @@ import com.android.launcher3.celllayout.DelegatedCellDrawing; import com.android.launcher3.celllayout.ItemConfiguration; import com.android.launcher3.celllayout.ReorderAlgorithm; import com.android.launcher3.celllayout.ReorderParameters; +import com.android.launcher3.celllayout.ReorderPreviewAnimation; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.folder.PreviewBackground; @@ -189,7 +183,7 @@ public class CellLayout extends ViewGroup { @ContainerType private final int mContainerType; - private final float mChildScale = 1f; + public static final float DEFAULT_SCALE = 1f; public static final int MODE_SHOW_REORDER_HINT = 0; public static final int MODE_DRAG_OVER = 1; @@ -199,8 +193,8 @@ public class CellLayout extends ViewGroup { private static final boolean DESTRUCTIVE_REORDER = false; private static final boolean DEBUG_VISUALIZE_OCCUPIED = false; - private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; - private static final int REORDER_ANIMATION_DURATION = 150; + public static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; + public static final int REORDER_ANIMATION_DURATION = 150; @Thunk final float mReorderPreviewAnimationMagnitude; private final ArrayList mIntersectingViews = new ArrayList<>(); @@ -219,6 +213,7 @@ public class CellLayout extends ViewGroup { // Related to accessible drag and drop DragAndDropAccessibilityDelegate mTouchHelper; + CellLayoutContainer mCellLayoutContainer; public static final FloatProperty SPRING_LOADED_PROGRESS = new FloatProperty("spring_loaded_progress") { @@ -233,8 +228,9 @@ public class CellLayout extends ViewGroup { } }; - public CellLayout(Context context) { - this(context, null); + public CellLayout(Context context, CellLayoutContainer container) { + this(context, (AttributeSet) null); + this.mCellLayoutContainer = container; } public CellLayout(Context context, AttributeSet attrs) { @@ -321,11 +317,18 @@ public class CellLayout extends ViewGroup { addView(mShortcutsAndWidgets); } + public CellLayoutContainer getCellLayoutContainer() { + return mCellLayoutContainer; + } + + public void setCellLayoutContainer(CellLayoutContainer cellLayoutContainer) { + mCellLayoutContainer = cellLayoutContainer; + } + /** * Sets or clears a delegate used for accessible drag and drop */ public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) { - setOnClickListener(delegate); ViewCompat.setAccessibilityDelegate(this, delegate); mTouchHelper = delegate; @@ -333,7 +336,6 @@ public class CellLayout extends ViewGroup { ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO; setImportantForAccessibility(accessibilityFlag); getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag); - // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared. setFocusable(delegate != null); // Invalidate the accessibility hierarchy @@ -578,9 +580,7 @@ public class CellLayout extends ViewGroup { } protected void updateBgAlpha() { - if (!getWorkspace().mLauncher.isInState(EDIT_MODE)) { - mBackground.setAlpha((int) (mSpringLoadedProgress * 255)); - } + mBackground.setAlpha((int) (mSpringLoadedProgress * 255)); } /** @@ -762,8 +762,8 @@ public class CellLayout extends ViewGroup { bubbleChild.setTextVisibility(mContainerType != HOTSEAT); } - child.setScaleX(mChildScale); - child.setScaleY(mChildScale); + child.setScaleX(DEFAULT_SCALE); + child.setScaleY(DEFAULT_SCALE); // Generate an id for each view, this assumes we have at most 256x256 cells // per workspace screen @@ -1192,7 +1192,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) { - int screenId = getWorkspace().getIdForScreen(this); + int screenId = mCellLayoutContainer.getCellLayoutId(this); cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect); ((LauncherAppWidgetHostView) view).handleDrag(mTempRect, this, screenId); @@ -1205,25 +1205,19 @@ public class CellLayout extends ViewGroup { return getContext().getString(R.string.move_to_hotseat_position, Math.max(cellX, cellY) + 1); } else { - Workspace workspace = getWorkspace(); int row = cellY + 1; - int col = workspace.mIsRtl ? mCountX - cellX : cellX + 1; - int panelCount = workspace.getPanelCount(); - int screenId = workspace.getIdForScreen(this); - int pageIndex = workspace.getPageIndexForScreenId(screenId); + int col = Utilities.isRtl(getResources()) ? mCountX - cellX : cellX + 1; + int panelCount = mCellLayoutContainer.getPanelCount(); + int pageIndex = mCellLayoutContainer.getCellLayoutIndex(this); if (panelCount > 1) { // Increment the column if the target is on the right side of a two panel home col += (pageIndex % panelCount) * mCountX; } return getContext().getString(R.string.move_to_empty_cell_description, row, col, - workspace.getPageDescription(pageIndex)); + mCellLayoutContainer.getPageDescription(pageIndex)); } } - private Workspace getWorkspace() { - return Launcher.cast(mActivity).getWorkspace(); - } - public void clearDragOutlines() { final int oldIndex = mDragOutlineCurrent; mDragOutlineAnims[oldIndex].animateOut(); @@ -1439,174 +1433,14 @@ public class CellLayout extends ViewGroup { CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); if (c != null && !skip && (child instanceof Reorderable)) { - ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, - mode, lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY); + ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, + lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY, + mReorderPreviewAnimationMagnitude, this, mShakeAnimators); rha.animate(); } } } - private static final Property ANIMATION_PROGRESS = - new Property(float.class, "animationProgress") { - @Override - public Float get(ReorderPreviewAnimation anim) { - return anim.animationProgress; - } - - @Override - public void set(ReorderPreviewAnimation anim, Float progress) { - anim.setAnimationProgress(progress); - } - }; - - // Class which represents the reorder preview animations. These animations show that an item is - // in a temporary state, and hint at where the item will return to. - class ReorderPreviewAnimation { - final T child; - float finalDeltaX; - float finalDeltaY; - float initDeltaX; - float initDeltaY; - final float finalScale; - float initScale; - final int mode; - boolean repeating = false; - private static final int PREVIEW_DURATION = 300; - private static final int HINT_DURATION = Workspace.REORDER_TIMEOUT; - - private static final float CHILD_DIVIDEND = 4.0f; - - public static final int MODE_HINT = 0; - public static final int MODE_PREVIEW = 1; - - float animationProgress = 0; - ValueAnimator a; - - ReorderPreviewAnimation(View childView, int mode, int cellX0, int cellY0, - int cellX1, int cellY1, int spanX, int spanY) { - regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint); - final int x0 = mTmpPoint[0]; - final int y0 = mTmpPoint[1]; - regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint); - final int x1 = mTmpPoint[0]; - final int y1 = mTmpPoint[1]; - final int dX = x1 - x0; - final int dY = y1 - y0; - - this.child = (T) childView; - this.mode = mode; - finalDeltaX = 0; - finalDeltaY = 0; - - MultiTranslateDelegate mtd = child.getTranslateDelegate(); - initDeltaX = mtd.getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).getValue(); - initDeltaY = mtd.getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).getValue(); - initScale = child.getReorderBounceScale(); - finalScale = mChildScale - (CHILD_DIVIDEND / child.getWidth()) * initScale; - - int dir = mode == MODE_HINT ? -1 : 1; - if (dX == dY && dX == 0) { - } else { - if (dY == 0) { - finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude; - } else if (dX == 0) { - finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude; - } else { - double angle = Math.atan( (float) (dY) / dX); - finalDeltaX = (int) (-dir * Math.signum(dX) - * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude)); - finalDeltaY = (int) (-dir * Math.signum(dY) - * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude)); - } - } - } - - void setInitialAnimationValuesToBaseline() { - initScale = mChildScale; - initDeltaX = 0; - initDeltaY = 0; - } - - void animate() { - boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0); - - if (mShakeAnimators.containsKey(child)) { - ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child); - mShakeAnimators.remove(child); - - if (noMovement) { - // A previous animation for this item exists, and no new animation will exist. - // Finish the old animation smoothly. - oldAnimation.finishAnimation(); - return; - } else { - // A previous animation for this item exists, and a new one will exist. Stop - // the old animation in its tracks, and proceed with the new one. - oldAnimation.cancel(); - } - } - if (noMovement) { - return; - } - - ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, 0, 1); - a = va; - - // Animations are disabled in power save mode, causing the repeated animation to jump - // spastically between beginning and end states. Since this looks bad, we don't repeat - // the animation in power save mode. - if (areAnimatorsEnabled()) { - va.setRepeatMode(ValueAnimator.REVERSE); - va.setRepeatCount(ValueAnimator.INFINITE); - } - - va.setDuration(mode == MODE_HINT ? HINT_DURATION : PREVIEW_DURATION); - va.setStartDelay((int) (Math.random() * 60)); - va.addListener(new AnimatorListenerAdapter() { - public void onAnimationRepeat(Animator animation) { - // We make sure to end only after a full period - setInitialAnimationValuesToBaseline(); - repeating = true; - } - }); - mShakeAnimators.put(child, this); - va.start(); - } - - private void setAnimationProgress(float progress) { - animationProgress = progress; - float r1 = (mode == MODE_HINT && repeating) ? 1.0f : animationProgress; - float x = r1 * finalDeltaX + (1 - r1) * initDeltaX; - float y = r1 * finalDeltaY + (1 - r1) * initDeltaY; - child.getTranslateDelegate().setTranslation(INDEX_REORDER_BOUNCE_OFFSET, x, y); - float s = animationProgress * finalScale + (1 - animationProgress) * initScale; - child.setReorderBounceScale(s); - } - - private void cancel() { - if (a != null) { - a.cancel(); - } - } - - /** - * Smoothly returns the item to its baseline position / scale - */ - @Thunk void finishAnimation() { - if (a != null) { - a.cancel(); - } - - setInitialAnimationValuesToBaseline(); - ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, - animationProgress, 0); - a = va; - a.setInterpolator(DECELERATE_1_5); - a.setDuration(REORDER_ANIMATION_DURATION); - a.start(); - } - } - private void completeAndClearReorderPreviewAnimations() { for (ReorderPreviewAnimation a: mShakeAnimators.values()) { a.finishAnimation(); @@ -1617,7 +1451,7 @@ public class CellLayout extends ViewGroup { private void commitTempPlacement(View dragView) { mTmpOccupied.copyTo(mOccupied); - int screenId = getWorkspace().getIdForScreen(this); + int screenId = mCellLayoutContainer.getCellLayoutId(this); int container = Favorites.CONTAINER_DESKTOP; if (mContainerType == HOTSEAT) { @@ -1718,7 +1552,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); setUseTempCoords(true); if (swapSolution != null && swapSolution.isSolution) { @@ -1747,13 +1581,13 @@ public class CellLayout extends ViewGroup { } protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, - int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, - ItemConfiguration solution) { + int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX) { ItemConfiguration configuration = new ItemConfiguration(); copyCurrentStateToSolution(configuration); ReorderParameters parameters = new ReorderParameters(pixelX, pixelY, spanX, spanY, minSpanX, minSpanY, dragView, configuration); - return createReorderAlgorithm().findReorderSolution(parameters, decX); + int[] directionVector = direction != null ? direction : mDirectionVector; + return createReorderAlgorithm().findReorderSolution(parameters, directionVector, decX); } public void copyCurrentStateToSolution(ItemConfiguration solution) { @@ -2077,7 +1911,7 @@ public class CellLayout extends ViewGroup { cellToPoint(cellX, cellY, cellPoint); if (findReorderSolution(cellPoint[0], cellPoint[1], itemInfo.minSpanX, itemInfo.minSpanY, itemInfo.spanX, itemInfo.spanY, mDirectionVector, null, - true, new ItemConfiguration()).isSolution) { + true).isSolution) { return true; } } @@ -2092,9 +1926,18 @@ public class CellLayout extends ViewGroup { int[] cellPoint = new int[2]; int[] directionVector = new int[]{0, -1}; cellToPoint(0, mCountY, cellPoint); - ItemConfiguration configuration = new ItemConfiguration(); - if (findReorderSolution(cellPoint[0], cellPoint[1], mCountX, 1, mCountX, 1, - directionVector, null, false, configuration).isSolution) { + ItemConfiguration configuration = findReorderSolution( + cellPoint[0] /* pixelX */, + cellPoint[1] /* pixelY */, + mCountX /* minSpanX */, + 1 /* minSpanY */, + mCountX /* spanX */, + 1 /* spanY */, + directionVector /* direction */, + null /* dragView */, + false /* decX */ + ); + if (configuration.isSolution) { if (commitConfig) { copySolutionToTempState(configuration, null); commitTempPlacement(null); diff --git a/src/com/android/launcher3/CellLayoutContainer.java b/src/com/android/launcher3/CellLayoutContainer.java new file mode 100644 index 0000000000..9ee0f70d4a --- /dev/null +++ b/src/com/android/launcher3/CellLayoutContainer.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 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; + +/** + * This interface should be implemented for any container/view that has a CellLayout as a children. + */ +public interface CellLayoutContainer { + + /** + * Get the CellLayoutId for the given cellLayout. + */ + int getCellLayoutId(CellLayout cellLayout); + + /** + * Get the index of the given CellLayout out of all the other CellLayouts. + */ + int getCellLayoutIndex(CellLayout cellLayout); + + /** + * The total number of CellLayouts in the container. + */ + int getPanelCount(); + + /** + * Used for accessibility, it returns the string that the assistant is going to say when + * referring to the given CellLayout. + */ + String getPageDescription(int pageIndex); +} diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 1ca7da9d44..f96d59ebba 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -78,10 +78,13 @@ import java.util.function.Consumer; public class DeviceProfile { private static final int DEFAULT_DOT_SIZE = 100; - private static final float ALL_APPS_TABLET_MAX_ROWS = 5.5f; private static final float MIN_FOLDER_TEXT_SIZE_SP = 16f; private static final float MIN_WIDGET_PADDING_DP = 6f; + // Minimum aspect ratio beyond which an extra top padding may be applied to a bottom sheet. + private static final float MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING = 1.5f; + private static final float MAX_ASPECT_RATIO_FOR_ALTERNATE_EDIT_STATE = 1.5f; + public static final PointF DEFAULT_SCALE = new PointF(1.0f, 1.0f); public static final ViewScaleProvider DEFAULT_PROVIDER = itemInfo -> DEFAULT_SCALE; public static final Consumer DEFAULT_DIMENSION_PROVIDER = dp -> { @@ -258,7 +261,6 @@ public class DeviceProfile { public int overviewTaskIconSizePx; public int overviewTaskIconDrawableSizePx; public int overviewTaskIconDrawableSizeGridPx; - public int overviewTaskIconAppChipMenuDrawableSizePx; public int overviewTaskThumbnailTopMarginPx; public final int overviewActionsHeight; public final int overviewActionsTopMarginPx; @@ -415,9 +417,18 @@ public class DeviceProfile { gridVisualizationPaddingY = res.getDimensionPixelSize( R.dimen.grid_visualization_vertical_cell_spacing); - bottomSheetTopPadding = mInsets.top // statusbar height - + res.getDimensionPixelSize(R.dimen.bottom_sheet_extra_top_padding) - + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding + { + // In large screens, in portrait mode, a bottom sheet can appear too elongated, so, we + // apply additional padding. + final boolean applyExtraTopPadding = isTablet + && !isLandscape + && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING); + final int derivedTopPadding = heightPx / 6; + bottomSheetTopPadding = mInsets.top // statusbar height + + (applyExtraTopPadding ? derivedTopPadding : 0) + + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding + } + bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration); bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration); if (isTablet) { @@ -493,8 +504,17 @@ public class DeviceProfile { } 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); + // Some foldable portrait modes are too wide in terms of aspect ratio so we need to tweak + // the dimensions for edit state. + final boolean shouldApplyWidePortraitDimens = isTablet + && !isLandscape + && aspectRatio < MAX_ASPECT_RATIO_FOR_ALTERNATE_EDIT_STATE; + dropTargetBarTopMarginPx = shouldApplyWidePortraitDimens + ? 0 + : res.getDimensionPixelSize(R.dimen.drop_target_top_margin); + dropTargetBarBottomMarginPx = shouldApplyWidePortraitDimens + ? res.getDimensionPixelSize(R.dimen.drop_target_bottom_margin_wide_portrait) + : 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( @@ -585,8 +605,9 @@ public class DeviceProfile { } } - springLoadedHotseatBarTopMarginPx = res.getDimensionPixelSize( - R.dimen.spring_loaded_hotseat_top_margin); + springLoadedHotseatBarTopMarginPx = shouldApplyWidePortraitDimens + ? res.getDimensionPixelSize(R.dimen.spring_loaded_hotseat_top_margin_wide_portrait) + : res.getDimensionPixelSize(R.dimen.spring_loaded_hotseat_top_margin); if (mIsResponsiveGrid) { updateHotseatSizes(mResponsiveWorkspaceCellSpec.getIconSize()); @@ -641,8 +662,8 @@ public class DeviceProfile { DimensionType.WIDTH, numShownAllAppsColumns, availableWidthPx, mResponsiveWorkspaceWidthSpec); mResponsiveAllAppsHeightSpec = allAppsSpecs.getCalculatedSpec(responsiveAspectRatio, - DimensionType.HEIGHT, inv.numRows, heightPx - mInsets.top, - mResponsiveWorkspaceHeightSpec); + DimensionType.HEIGHT, inv.numAllAppsRowsForCellHeightCalculation, + heightPx - mInsets.top, mResponsiveWorkspaceHeightSpec); ResponsiveSpecsProvider folderSpecs = ResponsiveSpecsProvider.create( new ResourceHelper(context, @@ -678,8 +699,6 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size); overviewTaskIconDrawableSizeGridPx = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid); - overviewTaskIconAppChipMenuDrawableSizePx = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_drawable_size); overviewTaskThumbnailTopMarginPx = enableOverviewIconMenu() ? 0 : overviewTaskIconSizePx + overviewTaskMarginPx; // Don't add margin with floating search bar to minimize risk of overlapping. @@ -709,7 +728,7 @@ public class DeviceProfile { } // Calculate all of the remaining variables. - extraSpace = updateAvailableDimensions(res); + extraSpace = updateAvailableDimensions(context); calculateAndSetWorkspaceVerticalPadding(context, inv, extraSpace); @@ -734,14 +753,9 @@ public class DeviceProfile { hotseatBorderSpace = cellLayoutBorderSpacePx.y; } - // AllApps height calculation depends on updated cellSize if (isTablet) { - int collapseHandleHeight = - res.getDimensionPixelOffset(R.dimen.bottom_sheet_handle_area_height); - int contentHeight = heightPx - collapseHandleHeight - hotseatQsbHeight; - int targetContentHeight = (int) (allAppsCellHeightPx * ALL_APPS_TABLET_MAX_ROWS); - allAppsPadding.top = Math.max(mInsets.top, contentHeight - targetContentHeight); - allAppsShiftRange = heightPx - allAppsPadding.top; + allAppsPadding.top = mInsets.top; + allAppsShiftRange = heightPx; } else { allAppsPadding.top = 0; allAppsShiftRange = @@ -789,14 +803,16 @@ public class DeviceProfile { * width of the hotseat. */ private int calculateQsbWidth(int hotseatBorderSpace) { + int iconExtraSpacePx = iconSizePx - getIconVisibleSizePx(iconSizePx); if (isQsbInline) { int columns = getPanelCount() * inv.numColumns; return getIconToIconWidthForColumns(columns) - iconSizePx * numShownHotseatIcons - - hotseatBorderSpace * numShownHotseatIcons; + - hotseatBorderSpace * numShownHotseatIcons + - iconExtraSpacePx; } else { int columns = inv.hotseatColumnSpan[mTypeIndex]; - return getIconToIconWidthForColumns(columns); + return getIconToIconWidthForColumns(columns) - iconExtraSpacePx; } } @@ -992,16 +1008,6 @@ public class DeviceProfile { float workspaceCellPaddingY = getCellSize().y - iconSizePx - iconDrawablePaddingPx - iconTextHeight; - if (mIsResponsiveGrid) { - iconTextSizePx = 0; - iconDrawablePaddingPx = 0; - int iconSizeWithOverlap = getIconSizeWithOverlap(iconSizePx); - cellYPaddingPx = Math.max(0, getCellSize().y - iconSizeWithOverlap) / 2; - autoResizeAllAppsCells(); - - return; - } - // We want enough space so that the text is closer to its corresponding icon. if (workspaceCellPaddingY < iconTextHeight) { iconTextSizePx = 0; @@ -1014,14 +1020,14 @@ public class DeviceProfile { /** * Returns the amount of extra (or unused) vertical space. */ - private int updateAvailableDimensions(Resources res) { - iconCenterVertically = mIsScalableGrid || mIsResponsiveGrid; + private int updateAvailableDimensions(Context context) { + iconCenterVertically = (mIsScalableGrid || mIsResponsiveGrid) && isVerticalBarLayout(); if (mIsResponsiveGrid) { iconSizePx = mResponsiveWorkspaceCellSpec.getIconSize(); iconTextSizePx = mResponsiveWorkspaceCellSpec.getIconTextSize(); mIconDrawablePaddingOriginalPx = mResponsiveWorkspaceCellSpec.getIconDrawablePadding(); - updateIconSize(1f, res); + updateIconSize(1f, context); updateWorkspacePadding(); return 0; } @@ -1031,7 +1037,7 @@ public class DeviceProfile { iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics)); iconTextSizePx = pxFromSp(invIconTextSizeSp, mMetrics); - updateIconSize(1f, res); + updateIconSize(1f, context); updateWorkspacePadding(); // Check to see if the icons fit within the available height. @@ -1055,7 +1061,7 @@ public class DeviceProfile { if (shouldScale) { float scale = Math.min(scaleX, scaleY); - updateIconSize(scale, res); + updateIconSize(scale, context); extraHeight = Math.max(0, maxHeight - getCellLayoutHeightSpecification()); } @@ -1074,11 +1080,8 @@ public class DeviceProfile { } private int getNormalizedIconDrawablePadding(int iconSizePx, int iconDrawablePadding) { - // TODO(b/235886078): workaround needed because of this bug - // Icons are 10% larger on XML than their visual size, - // so remove that extra space to get labels closer to the correct padding - int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx); - return Math.max(0, iconDrawablePadding - ((iconSizePx - iconVisibleSizePx) / 2)); + return Math.max(0, iconDrawablePadding + - ((iconSizePx - getIconVisibleSizePx(iconSizePx)) / 2)); } private int getNormalizedIconDrawablePadding() { @@ -1091,8 +1094,7 @@ public class DeviceProfile { // so remove that extra space to get labels closer to the correct padding int drawablePadding = (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3; - int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * folderChildIconSizePx); - int iconSizeDiff = folderChildIconSizePx - iconVisibleSizePx; + int iconSizeDiff = folderChildIconSizePx - getIconVisibleSizePx(folderChildIconSizePx); return Math.max(0, drawablePadding - iconSizeDiff / 2); } @@ -1105,7 +1107,7 @@ public class DeviceProfile { * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants, * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx. */ - public void updateIconSize(float scale, Resources res) { + public void updateIconSize(float scale, Context context) { // Icon scale should never exceed 1, otherwise pixellation may occur. iconScale = Math.min(1f, scale); cellScaleToFit = scale; @@ -1123,25 +1125,28 @@ public class DeviceProfile { iconSizePx = mIconSizeSteps.getIconSmallerThan(cellWidthPx); } - iconDrawablePaddingPx = getNormalizedIconDrawablePadding(); + if (isVerticalLayout) { + iconDrawablePaddingPx = 0; + iconTextSizePx = 0; + } else { + iconDrawablePaddingPx = getNormalizedIconDrawablePadding(); + } CellContentDimensions cellContentDimensions = new CellContentDimensions(iconSizePx, iconDrawablePaddingPx, iconTextSizePx); - if (isVerticalLayout) { - if (cellHeightPx < iconSizePx) { - cellContentDimensions.setIconSizePx( - mIconSizeSteps.getIconSmallerThan(cellHeightPx)); - } - } else { - cellContentDimensions.resizeToFitCellHeight(cellHeightPx, mIconSizeSteps); - } + int cellContentHeight = cellContentDimensions.resizeToFitCellHeight(cellHeightPx, + mIconSizeSteps); iconSizePx = cellContentDimensions.getIconSizePx(); iconDrawablePaddingPx = cellContentDimensions.getIconDrawablePaddingPx(); iconTextSizePx = cellContentDimensions.getIconTextSizePx(); - int cellContentHeight = cellContentDimensions.getCellContentHeight(); - cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2; + if (isVerticalLayout) { + cellYPaddingPx = Math.max(0, getCellSize().y - getIconSizeWithOverlap(iconSizePx)) + / 2; + } else { + cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2; + } } else if (mIsScalableGrid) { iconDrawablePaddingPx = (int) (getNormalizedIconDrawablePadding() * iconScale); cellWidthPx = pxFromDp(inv.minCellSize[mTypeIndex].x, mMetrics, scale); @@ -1222,13 +1227,14 @@ public class DeviceProfile { if (mIsResponsiveGrid) { updateAllAppsWithResponsiveMeasures(); } else { - updateAllAppsIconSize(scale, res); + updateAllAppsIconSize(scale, context.getResources()); } updateAllAppsContainerWidth(); - if (isVerticalBarLayout()) { + if (isVerticalLayout && !mIsResponsiveGrid) { hideWorkspaceLabelsIfNotEnoughSpace(); } - if (FeatureFlags.enableTwolineAllapps()) { + if ((Flags.enableTwolineToggle() + && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context))) { // Add extra textHeight to the existing allAppsCellHeight. allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx); } @@ -1349,7 +1355,7 @@ public class DeviceProfile { if (allAppsCellHeightPx < cellContentDimensions.getCellContentHeight()) { if (isVerticalBarLayout()) { - if (allAppsCellHeightPx < iconSizePx) { + if (allAppsCellHeightPx < allAppsIconSizePx) { cellContentDimensions.setIconSizePx( mIconSizeSteps.getIconSmallerThan(allAppsCellHeightPx)); } @@ -1363,6 +1369,10 @@ public class DeviceProfile { } allAppsCellHeightPx += mResponsiveAllAppsHeightSpec.getGutterPx(); + + if (isVerticalBarLayout()) { + autoResizeAllAppsCells(); + } } /** @@ -1740,15 +1750,8 @@ public class DeviceProfile { // 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 = mIsResponsiveGrid ? 0 - : 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); + int paddingTop = Math.max((int) (mInsets.top + cellLayoutPaddingPx.top), 0); + int paddingBottom = Math.max((int) (mInsets.bottom + cellLayoutPaddingPx.bottom), 0); if (isSeascape()) { hotseatBarPadding.set(mInsets.left + mHotseatBarEdgePaddingPx, paddingTop, @@ -1788,7 +1791,8 @@ public class DeviceProfile { } } else if (mIsScalableGrid) { - int sideSpacing = (availableWidthPx - hotseatQsbWidth) / 2; + int iconExtraSpacePx = iconSizePx - getIconVisibleSizePx(iconSizePx); + int sideSpacing = (availableWidthPx - (hotseatQsbWidth + iconExtraSpacePx)) / 2; hotseatBarPadding.set(sideSpacing, 0, sideSpacing, @@ -1827,13 +1831,24 @@ public class DeviceProfile { availableWidthPx - allAppsSpacing, 0 /* borderSpace */, numShownAllAppsColumns); - int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * allAppsIconSizePx); - int iconAlignmentMargin = (cellWidth - iconVisibleSize) / 2; + int iconAlignmentMargin = (cellWidth - getIconVisibleSizePx(allAppsIconSizePx)) / 2; return (Utilities.isRtl(context.getResources()) ? allAppsPadding.right : allAppsPadding.left) + iconAlignmentMargin; } + /** + * TODO(b/235886078): workaround needed because of this bug + * Icons are 10% larger on XML than their visual size, so remove that extra space to get + * some dimensions correct. + * + * When this bug is resolved this method will no longer be needed and we would be able to + * replace all instances where this method is called with iconSizePx. + */ + private int getIconVisibleSizePx(int iconSizePx) { + return Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx); + } + private int getAdditionalQsbSpace() { return isQsbInline ? hotseatQsbWidth + hotseatBorderSpace : 0; } @@ -2153,8 +2168,6 @@ public class DeviceProfile { overviewTaskIconDrawableSizePx)); writer.println(prefix + pxToDpStr("overviewTaskIconDrawableSizeGridPx", overviewTaskIconDrawableSizeGridPx)); - writer.println(prefix + pxToDpStr("overviewTaskIconAppChipMenuDrawableSizePx", - overviewTaskIconAppChipMenuDrawableSizePx)); writer.println(prefix + pxToDpStr("overviewTaskThumbnailTopMarginPx", overviewTaskThumbnailTopMarginPx)); writer.println(prefix + pxToDpStr("overviewActionsTopMarginPx", diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java index ec26f5849c..fe9348c451 100644 --- a/src/com/android/launcher3/ExtendedEditText.java +++ b/src/com/android/launcher3/ExtendedEditText.java @@ -15,8 +15,6 @@ */ package com.android.launcher3; -import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW; - import android.content.Context; import android.graphics.Rect; import android.text.TextUtils; @@ -93,7 +91,6 @@ public class ExtendedEditText extends EditText { * @return true if the keyboard is shown correctly and focus is given to this view. */ public boolean showKeyboard() { - onKeyboardShown(); return requestFocus() && showSoftInputInternal(); } @@ -120,11 +117,6 @@ public class ExtendedEditText extends EditText { } } - protected void onKeyboardShown() { - ActivityContext.lookupContext(getContext()).getStatsLogManager() - .keyboardStateManager().setKeyboardState(SHOW); - } - private boolean showSoftInputInternal() { boolean result = false; InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java index a13dcc1bc3..51c7a055ca 100644 --- a/src/com/android/launcher3/FastScrollRecyclerView.java +++ b/src/com/android/launcher3/FastScrollRecyclerView.java @@ -197,11 +197,10 @@ public abstract class FastScrollRecyclerView extends RecyclerView { /** * Scrolls this recycler view to the bottom with easing and duration. */ - public void scrollToBottomWithMotion() { + public void scrollToBottomWithMotion(int duration) { if (mScrollbar != null) { mScrollbar.reattachThumbToScroll(); } - // Emphasized interpolators with 500ms duration - smoothScrollBy(0, getAvailableScrollHeight(), Interpolators.EMPHASIZED, 500); + smoothScrollBy(0, getAvailableScrollHeight(), Interpolators.EMPHASIZED, duration); } } diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java index c782dca4ca..9ef6edc6cf 100644 --- a/src/com/android/launcher3/GestureNavContract.java +++ b/src/com/android/launcher3/GestureNavContract.java @@ -20,11 +20,9 @@ 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; @@ -69,7 +67,6 @@ public class GestureNavContract { /** * Sends the position information to the receiver */ - @TargetApi(Build.VERSION_CODES.R) public void sendEndPosition(RectF position, ActivityContext context, @Nullable SurfaceControl surfaceControl) { Bundle result = new Bundle(); @@ -95,9 +92,6 @@ public class GestureNavContract { * Clears and returns the GestureNavContract if it was present in the intent. */ public static GestureNavContract fromIntent(Intent intent) { - if (!Utilities.ATLEAST_R) { - return null; - } Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT); if (extras == null) { return null; diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index 3b12b86bf7..37737d81a3 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -207,6 +207,7 @@ public class Hotseat extends CellLayout implements Insettable { public void setWorkspace(Workspace w) { mWorkspace = w; + setCellLayoutContainer(w); } @Override diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 5721ed3493..42d4d50218 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -170,6 +170,7 @@ public class InvariantDeviceProfile { * Number of columns in the all apps list. */ public int numAllAppsColumns; + public int numAllAppsRowsForCellHeightCalculation; public int numDatabaseAllAppsColumns; public @StyleRes int allAppsStyle; @@ -247,7 +248,7 @@ public class InvariantDeviceProfile { public InvariantDeviceProfile(Context context, String gridName) { String newName = initGrid(context, gridName); if (newName == null || !newName.equals(gridName)) { - throw new IllegalArgumentException("Unknown grid name"); + throw new IllegalArgumentException("Unknown grid name: " + gridName); } } @@ -393,6 +394,8 @@ public class InvariantDeviceProfile { workspaceCellSpecsTwoPanelId = closestProfile.mWorkspaceCellSpecsTwoPanelId; allAppsCellSpecsId = closestProfile.mAllAppsCellSpecsId; allAppsCellSpecsTwoPanelId = closestProfile.mAllAppsCellSpecsTwoPanelId; + numAllAppsRowsForCellHeightCalculation = + closestProfile.mNumAllAppsRowsForCellHeightCalculation; this.deviceType = deviceType; inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing; @@ -423,6 +426,7 @@ public class InvariantDeviceProfile { allAppsStyle = closestProfile.allAppsStyle; numAllAppsColumns = closestProfile.numAllAppsColumns; + numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns; @@ -821,6 +825,7 @@ public class InvariantDeviceProfile { private final @StyleRes int allAppsStyle; private final int numAllAppsColumns; + private final int mNumAllAppsRowsForCellHeightCalculation; private final int numDatabaseAllAppsColumns; private final int numHotseatIcons; private final int numDatabaseHotseatIcons; @@ -943,34 +948,37 @@ public class InvariantDeviceProfile { R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE); mWorkspaceSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_workspaceSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mWorkspaceSpecsId); mAllAppsSpecsId = a.getResourceId( R.styleable.GridDisplayOption_allAppsSpecsId, INVALID_RESOURCE_HANDLE); mAllAppsSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_allAppsSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mAllAppsSpecsId); mFolderSpecsId = a.getResourceId( R.styleable.GridDisplayOption_folderSpecsId, INVALID_RESOURCE_HANDLE); mFolderSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_folderSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mFolderSpecsId); mHotseatSpecsId = a.getResourceId( R.styleable.GridDisplayOption_hotseatSpecsId, INVALID_RESOURCE_HANDLE); mHotseatSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mHotseatSpecsId); mWorkspaceCellSpecsId = a.getResourceId( R.styleable.GridDisplayOption_workspaceCellSpecsId, INVALID_RESOURCE_HANDLE); mWorkspaceCellSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_workspaceCellSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mWorkspaceCellSpecsId); mAllAppsCellSpecsId = a.getResourceId( R.styleable.GridDisplayOption_allAppsCellSpecsId, INVALID_RESOURCE_HANDLE); mAllAppsCellSpecsTwoPanelId = a.getResourceId( R.styleable.GridDisplayOption_allAppsCellSpecsTwoPanelId, - INVALID_RESOURCE_HANDLE); + mAllAppsCellSpecsId); + mNumAllAppsRowsForCellHeightCalculation = a.getInt( + R.styleable.GridDisplayOption_numAllAppsRowsForCellHeightCalculation, + numRows); } else { mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE; mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE; @@ -984,6 +992,7 @@ public class InvariantDeviceProfile { mWorkspaceCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE; mAllAppsCellSpecsId = INVALID_RESOURCE_HANDLE; mAllAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE; + mNumAllAppsRowsForCellHeightCalculation = numRows; } int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb, diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index e41a8a544c..c1ebbe58a2 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -19,6 +19,7 @@ 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_UI_MODE; +import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; import static com.android.app.animation.Interpolators.EMPHASIZED; @@ -26,6 +27,8 @@ import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER; import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; +import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2; +import static com.android.launcher3.Flags.enableWorkspaceInflation; import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY; import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; @@ -66,6 +69,8 @@ import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL; import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE; import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE; +import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE; +import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW; 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; @@ -74,6 +79,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED; @@ -85,7 +91,6 @@ import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.L import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.WARM; import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED; import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP; -import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID; import static com.android.launcher3.popup.SystemShortcut.APP_INFO; import static com.android.launcher3.popup.SystemShortcut.INSTALL; import static com.android.launcher3.popup.SystemShortcut.WIDGETS; @@ -97,7 +102,6 @@ import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch; import static com.android.launcher3.util.SettingsCache.TOUCHPAD_NATURAL_SCROLLING; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.TargetApi; @@ -115,6 +119,8 @@ import android.content.IntentSender; import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; @@ -130,6 +136,7 @@ import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -139,6 +146,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.animation.OvershootInterpolator; @@ -152,6 +161,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import androidx.window.embedding.RuleController; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; @@ -191,7 +201,6 @@ import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.ItemInstallQueue; 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; import com.android.launcher3.model.data.ItemInfo; @@ -203,7 +212,6 @@ import com.android.launcher3.pm.PinRequestHelper; import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.qsb.QsbContainerView; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.statemanager.StatefulActivity; @@ -220,6 +228,7 @@ import com.android.launcher3.util.CannedAnimationCoordinator; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; +import com.android.launcher3.util.ItemInflater; import com.android.launcher3.util.KeyboardShortcutsDelegate; import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.PackageUserKey; @@ -233,7 +242,6 @@ import com.android.launcher3.util.Themes; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.ComposeInitializer; import com.android.launcher3.views.FloatingIconView; @@ -254,13 +262,12 @@ import com.android.launcher3.widget.picker.WidgetsFullSheet; import com.android.systemui.plugins.LauncherOverlayPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.shared.LauncherOverlayManager; -import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay; -import com.android.wm.shell.Flags; +import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy; +import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -312,10 +319,6 @@ public class Launcher extends StatefulActivity private static final FloatProperty HOTSEAT_WIDGET_SCALE = HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WIDGET_TRANSITION); - private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing(); - private static final boolean DESKTOP_MODE_SUPPORTED = - "1".equals(Utilities.getSystemProperty("persist.wm.debug.desktop_mode_2", "0")); - private final ModelCallbacks mModelCallbacks = createModelCallbacks(); private final KeyboardShortcutsDelegate mKeyboardShortcutsDelegate = @@ -328,6 +331,7 @@ public class Launcher extends StatefulActivity private WidgetManagerHelper mAppWidgetManager; private LauncherWidgetHolder mAppWidgetHolder; + private ItemInflater mItemInflater; private final int[] mTmpAddItemCellCoordinates = new int[2]; @@ -510,11 +514,14 @@ public class Launcher extends StatefulActivity mStateManager = new StateManager<>(this, NORMAL); setupViews(); + updateDisallowBack(); mAppWidgetManager = new WidgetManagerHelper(this); mAppWidgetHolder = createAppWidgetHolder(); mAppWidgetHolder.startListening(); mAppWidgetHolder.addProviderChangeListener(() -> refreshAndBindWidgetsForPackageUser(null)); + mItemInflater = new ItemInflater<>(this, mAppWidgetHolder, getItemOnClickListener(), + mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace)); mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots); @@ -572,11 +579,14 @@ public class Launcher extends StatefulActivity mRotationHelper.initialize(); TraceHelper.INSTANCE.endSection(); - if (Utilities.ATLEAST_R) { - getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING); - } + getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING); setTitle(R.string.home_screen); mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE); + + if (com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) { + RuleController.getInstance(this).setRules( + RuleController.parseRules(this, R.xml.split_configuration)); + } } protected ModelCallbacks createModelCallbacks() { @@ -819,7 +829,7 @@ public class Launcher extends StatefulActivity announceForAccessibility(R.string.item_added_to_workspace); break; case REQUEST_CREATE_APPWIDGET: - completeAddAppWidget(appWidgetId, info, null, null); + completeAddAppWidget(appWidgetId, info, null, null, false, null); break; case REQUEST_RECONFIGURE_APPWIDGET: getStatsLogManager().logger().withItemInfo(info).log(LAUNCHER_WIDGET_RECONFIGURED); @@ -1006,11 +1016,18 @@ public class Launcher extends StatefulActivity AppWidgetHostView boundWidget = null; if (resultCode == RESULT_OK) { animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION; - final AppWidgetHostView layout = mAppWidgetHolder.createView(this, appWidgetId, - requestArgs.getWidgetHandler().getProviderInfo(this)); + + // Now that we are exiting the config activity with RESULT_OK. + // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we can retrieve the + // PendingAppWidgetHostView from LauncherWidgetHolder (it was added to + // LauncherWidgetHolder when starting the config activity). + final AppWidgetHostView layout = enableAddAppWidgetViaConfigActivityV2() + ? getWorkspace().getWidgetForAppWidgetId(appWidgetId) + : mAppWidgetHolder.createView(appWidgetId, + requestArgs.getWidgetHandler().getProviderInfo(this)); boundWidget = layout; onCompleteRunnable = () -> { - completeAddAppWidget(appWidgetId, requestArgs, layout, null); + completeAddAppWidget(appWidgetId, requestArgs, layout, null, false, null); if (!isInState(EDIT_MODE)) { mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); } @@ -1075,6 +1092,25 @@ public class Launcher extends StatefulActivity DiscoveryBounce.showForHomeIfNeeded(this); mAppWidgetHolder.setActivityResumed(true); + + // Listen for IME changes to keep state up to date. + getRootView().setWindowInsetsAnimationCallback( + new WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + @Override + public WindowInsets onProgress(WindowInsets windowInsets, + List windowInsetsAnimations) { + return windowInsets; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + WindowInsets insets = getRootView().getRootWindowInsets(); + boolean isImeVisible = + insets != null && insets.isVisible(WindowInsets.Type.ime()); + getStatsLogManager().keyboardStateManager().setKeyboardState( + isImeVisible ? SHOW : HIDE); + } + }); } private void logStopAndResume(boolean isResume) { @@ -1344,35 +1380,6 @@ public class Launcher extends StatefulActivity return super.onCreateView(parent, name, context, attrs); } - /** - * Creates a view representing a shortcut. - * - * @param info The data structure describing the shortcut. - */ - View createShortcut(WorkspaceItemInfo 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. 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. - */ - public View createShortcut(@Nullable ViewGroup parent, WorkspaceItemInfo info) { - BubbleTextView favorite = - (BubbleTextView) LayoutInflater.from(parent != null ? parent.getContext() : this) - .inflate(R.layout.app_icon, parent, false); - favorite.applyFromWorkspaceItem(info); - favorite.setOnClickListener(getItemOnClickListener()); - favorite.setOnFocusChangeListener(mFocusHandler); - return favorite; - } - /** * Add a shortcut to the workspace or to a Folder. * @@ -1396,7 +1403,7 @@ public class Launcher extends StatefulActivity if (container < 0) { // Adding a shortcut to the Workspace. - final View view = createShortcut(info); + final View view = mItemInflater.inflateItem(info, getModelWriter()); boolean foundCellSpan = false; // First we check if we already know the exact location where we want to add this item. if (cellX >= 0 && cellY >= 0) { @@ -1450,16 +1457,17 @@ public class Launcher extends StatefulActivity */ @Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo, - AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) { + @Nullable AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo, + boolean showPendingWidget, @Nullable Bitmap widgetPreviewBitmap) { if (appWidgetInfo == null) { appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId, itemInfo.getTargetComponent()); } - if (hostView == null) { + if (hostView == null && !showPendingWidget) { // Perform actual inflation because we're live - hostView = mAppWidgetHolder.createView(this, appWidgetId, appWidgetInfo); + hostView = mAppWidgetHolder.createView(appWidgetId, appWidgetInfo); } LauncherAppWidgetInfo launcherInfo; @@ -1471,47 +1479,72 @@ public class Launcher extends StatefulActivity launcherInfo.minSpanX = itemInfo.minSpanX; launcherInfo.minSpanY = itemInfo.minSpanY; launcherInfo.user = appWidgetInfo.getProfile(); + CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo); + if (showPendingWidget) { + launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY; + PendingAppWidgetHostView pendingAppWidgetHostView = new PendingAppWidgetHostView( + this, mAppWidgetHolder, launcherInfo, appWidgetInfo); + pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap); + hostView = pendingAppWidgetHostView; + } else if (hostView instanceof PendingAppWidgetHostView) { + ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null); + // User has selected a widget config and exited the config activity, we can trigger + // re-inflation of PendingAppWidgetHostView to replace it with + // LauncherAppWidgetHostView in workspace. + completeRestoreAppWidget(appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED); + + // Show resize frame on the newly inflated LauncherAppWidgetHostView. + LauncherAppWidgetHostView reInflatedHostView = + getWorkspace().getWidgetForAppWidgetId(appWidgetId); + showWidgetResizeFrame( + reInflatedHostView, + (LauncherAppWidgetInfo) reInflatedHostView.getTag(), + presenterPos); + return; + } if (itemInfo instanceof PendingAddWidgetInfo) { launcherInfo.sourceContainer = ((PendingAddWidgetInfo) itemInfo).sourceContainer; } else if (itemInfo instanceof PendingRequestArgs) { launcherInfo.sourceContainer = ((PendingRequestArgs) itemInfo).getWidgetSourceContainer(); } - CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo); getModelWriter().addItemToDatabase(launcherInfo, itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY); hostView.setVisibility(View.VISIBLE); - prepareAppWidget(hostView, launcherInfo); - mWorkspace.addInScreen(hostView, launcherInfo); + mItemInflater.prepareAppWidget(hostView, launcherInfo); + if (!enableAddAppWidgetViaConfigActivityV2() || hostView.getParent() == null) { + mWorkspace.addInScreen(hostView, launcherInfo); + } announceForAccessibility(R.string.item_added_to_workspace); // Show the widget resize frame. if (hostView instanceof LauncherAppWidgetHostView) { final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView; - CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId); - if (mStateManager.getState() == NORMAL) { - AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout); - } else { - mStateManager.addStateListener(new StateManager.StateListener() { - @Override - public void onStateTransitionComplete(LauncherState finalState) { - if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE) - && finalState == NORMAL) { - AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout); - mStateManager.removeStateListener(this); - } - } - }); - } + showWidgetResizeFrame(launcherHostView, launcherInfo, presenterPos); } } - private void prepareAppWidget(AppWidgetHostView hostView, LauncherAppWidgetInfo item) { - hostView.setTag(item); - item.onBindAppWidget(this, hostView); - hostView.setFocusable(true); - hostView.setOnFocusChangeListener(mFocusHandler); + /** Show widget resize frame. */ + private void showWidgetResizeFrame( + LauncherAppWidgetHostView launcherHostView, + LauncherAppWidgetInfo launcherInfo, + CellPos presenterPos) { + CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId); + if (mStateManager.getState() == NORMAL) { + AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout); + } else { + mStateManager.addStateListener(new StateManager.StateListener() { + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE) + && finalState == NORMAL) { + AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout); + mStateManager.removeStateListener(this); + } + } + }); + } } private final ScreenOnListener mScreenOnListener = this::onScreenOnChanged; @@ -1587,7 +1620,7 @@ public class Launcher extends StatefulActivity } if (FeatureFlags.enableSplitContextually()) { - handleSplitAnimationGoingToHome(); + handleSplitAnimationGoingToHome(LAUNCHER_SPLIT_SELECTION_EXIT_HOME); } mOverlayManager.hideOverlay(isStarted() && !isForceInvisible()); handleGestureContract(intent); @@ -1603,7 +1636,7 @@ public class Launcher extends StatefulActivity } /** Handle animating away split placeholder view when user taps on home button */ - protected void handleSplitAnimationGoingToHome() { + protected void handleSplitAnimationGoingToHome(EventEnum splitDismissReason) { // Overridden } @@ -1710,11 +1743,7 @@ public class Launcher extends StatefulActivity mModel.removeCallbacks(this); mRotationHelper.destroy(); - try { - mAppWidgetHolder.stopListening(); - } catch (NullPointerException ex) { - Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex); - } + mAppWidgetHolder.stopListening(); mAppWidgetHolder.destroy(); TextKeyListener.getInstance().release(); @@ -1771,19 +1800,39 @@ public class Launcher extends StatefulActivity addAppWidgetImpl(appWidgetId, info, boundWidget, addFlowHandler, 0); } + /** + * If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we always add widget + * host view to workspace, otherwise we only add widget to host view if config activity is + * not started. + */ void addAppWidgetImpl(int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) { - if (!addFlowHandler.startConfigActivity(this, appWidgetId, info, - REQUEST_CREATE_APPWIDGET)) { - // If the configuration flow was not started, add the widget + final boolean isActivityStarted = addFlowHandler.startConfigActivity( + this, appWidgetId, info, REQUEST_CREATE_APPWIDGET); - // Exit spring loaded mode if necessary after adding the widget - Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null - : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); - completeAddAppWidget(appWidgetId, info, boundWidget, - addFlowHandler.getProviderInfo(this)); - mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete); + if (!enableAddAppWidgetViaConfigActivityV2() && isActivityStarted) { + return; } + + // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled and config activity is + // started, we should remove the dropped AppWidgetHostView from drag layer and extract the + // Bitmap that shows the preview. Then pass the Bitmap to completeAddAppWidget() to create + // a PendingWidgetHostView. + Bitmap widgetPreviewBitmap = null; + if (isActivityStarted) { + DragView dropView = getDragLayer().clearAnimatedView(); + if (dropView != null && dropView.containsAppWidgetHostView()) { + widgetPreviewBitmap = getBitmapFromView(dropView.getContentView()); + } + } + + // Exit spring loaded mode if necessary after adding the widget + Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null + : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); + completeAddAppWidget(appWidgetId, info, boundWidget, + addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(), + widgetPreviewBitmap); + mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete); } public void addPendingItem(PendingAddItemInfo info, int container, int screenId, @@ -2135,82 +2184,36 @@ public class Launcher extends StatefulActivity */ @Override public void bindItems(final List items, final boolean forceAnimateIcons) { - bindItems(items, forceAnimateIcons, /* focusFirstItemForAccessibility= */ false); + bindInflatedItems(items.stream().map(i -> Pair.create( + i, getItemInflater().inflateItem(i, getModelWriter()))).toList(), + forceAnimateIcons ? new AnimatorSet() : null); } + @Override + public void bindInflatedItems(List> items) { + bindInflatedItems(items, null); + } /** - * Bind the items start-end from the list. + * Bind all the items in the map, ignoring any null views * - * Implementation of the method from LauncherModel.Callbacks. - * - * @param focusFirstItemForAccessibility true iff the first item to be added to the workspace - * should be focused for accessibility. + * @param boundAnim if non-null, uses it to create and play the bounce animation for added views */ - public void bindItems( - final List items, - final boolean forceAnimateIcons, - final boolean focusFirstItemForAccessibility) { + public void bindInflatedItems( + List> shortcuts, @Nullable AnimatorSet boundAnim) { // 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; int newItemsScreenId = -1; - int end = items.size(); - View newView = null; - for (int i = 0; i < end; i++) { - final ItemInfo item = items.get(i); + int index = 0; + for (Pair e : shortcuts) { + final ItemInfo item = e.first; - // Short circuit if we are loading dock items for a configuration which has no dock - if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && - mHotseat == null) { - continue; - } - - final View view; - switch (item.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: - case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: { - WorkspaceItemInfo info = (WorkspaceItemInfo) item; - view = createShortcut(info); - break; - } - case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: { - view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this, - (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), - (FolderInfo) item); - break; - } - case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: { - view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, - (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), - (FolderInfo) item); - break; - } - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: { - view = inflateAppWidget((LauncherAppWidgetInfo) item); - if (view == null) { - continue; - } - break; - } - default: - throw new RuntimeException("Invalid Item Type"); - } - - /* - * Remove colliding items. - */ + // Remove colliding items. CellPos presenterPos = getCellPosMapper().mapModelToPresenter(item); if (item.container == CONTAINER_DESKTOP) { CellLayout cl = mWorkspace.getScreenWithId(presenterPos.screenId); if (cl != null && cl.isOccupied(presenterPos.cellX, presenterPos.cellY)) { - View v = cl.getChildAt(presenterPos.cellX, presenterPos.cellY); - if (v == null) { - Log.e(TAG, "bindItems failed when removing colliding item=" + item); - } - Object tag = v.getTag(); + Object tag = cl.getChildAt(presenterPos.cellX, presenterPos.cellY).getTag(); String desc = "Collision while binding workspace item: " + item + ". Collides with " + tag; if (FeatureFlags.IS_STUDIO_BUILD) { @@ -2221,58 +2224,42 @@ public class Launcher extends StatefulActivity } } } + + View view = e.second; + if (view == null) { + continue; + } + if (enableWorkspaceInflation() && view instanceof LauncherAppWidgetHostView lv) { + view = getAppWidgetHolder().attachViewToHostAndGetAttachedView(lv); + } workspace.addInScreenFromBind(view, item); - if (forceAnimateIcons) { + if (boundAnim != null) { // Animate all the applications up now view.setAlpha(0f); view.setScaleX(0f); view.setScaleY(0f); - bounceAnims.add(createNewAppBounceAnimation(view, i)); + boundAnim.play(createNewAppBounceAnimation(view, index++)); newItemsScreenId = presenterPos.screenId; } - - if (newView == null) { - newView = view; - } } - View viewToFocus = newView; - // Animate to the correct pager - if (forceAnimateIcons && newItemsScreenId > -1) { - AnimatorSet anim = new AnimatorSet(); - anim.playTogether(bounceAnims); - if (focusFirstItemForAccessibility && viewToFocus != null) { - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - viewToFocus.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - }); - } - + // Animate to the correct page + if (boundAnim != null && newItemsScreenId > -1) { int currentScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getNextPage()); final int newScreenIndex = mWorkspace.getPageIndexForScreenId(newItemsScreenId); - final Runnable startBounceAnimRunnable = anim::start; + final Runnable startBounceAnimRunnable = boundAnim::start; - if (canAnimatePageChange && newItemsScreenId != currentScreenId) { + if (canAnimatePageChange() && newItemsScreenId != currentScreenId) { // We post the animation slightly delayed to prevent slowdowns // when we are loading right after we return to launcher. - mWorkspace.postDelayed(new Runnable() { - public void run() { - if (mWorkspace != null) { - closeOpenViews(false); - - mWorkspace.snapToPage(newScreenIndex); - mWorkspace.postDelayed(startBounceAnimRunnable, - NEW_APPS_ANIMATION_DELAY); - } - } + mWorkspace.postDelayed(() -> { + closeOpenViews(false); + mWorkspace.snapToPage(newScreenIndex); + mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY); }, NEW_APPS_PAGE_MOVE_DELAY); } else { mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY); } - } else if (focusFirstItemForAccessibility && viewToFocus != null) { - viewToFocus.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } workspace.requestLayout(); } @@ -2281,166 +2268,13 @@ public class Launcher extends StatefulActivity * Add the views for a widget to the workspace. */ public void bindAppWidget(LauncherAppWidgetInfo item) { - View view = inflateAppWidget(item); + View view = mItemInflater.inflateItem(item, getModelWriter()); if (view != null) { mWorkspace.addInScreen(view, item); mWorkspace.requestLayout(); } } - private View inflateAppWidget(LauncherAppWidgetInfo item) { - if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) { - item.providerName = QsbContainerView.getSearchComponentName(this); - if (item.providerName == null) { - getModelWriter().deleteItemFromDatabase(item, - "search widget removed because search component cannot be found"); - return null; - } - } - final AppWidgetHostView view; - if (mIsSafeModeEnabled) { - view = new PendingAppWidgetHostView(this, item, mIconCache, true); - prepareAppWidget(view, item); - return view; - } - - TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId); - - try { - final LauncherAppWidgetProviderInfo appWidgetInfo; - String removalReason = ""; - - if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) { - // If the provider is not ready, bind as a pending widget. - appWidgetInfo = null; - removalReason = "the provider isn't ready."; - } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { - // The widget id is not valid. Try to find the widget based on the provider info. - appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user); - if (appWidgetInfo == null) { - if (WidgetsModel.GO_DISABLE_WIDGETS) { - removalReason = "widgets are disabled on go device."; - } else { - removalReason = - "WidgetManagerHelper cannot find a provider from provider info."; - } - } - } else { - appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId, - item.getTargetComponent()); - if (appWidgetInfo == null) { - if (item.appWidgetId <= CUSTOM_WIDGET_ID) { - removalReason = - "CustomWidgetManager cannot find provider from that widget id."; - } else { - removalReason = "AppWidgetManager cannot find provider for that widget id." - + " It could be because AppWidgetService is not available, or the" - + " appWidgetId has not been bound to a the provider yet, or you" - + " don't have access to that appWidgetId."; - } - } - } - - // If the provider is ready, but the width is not yet restored, try to restore it. - if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) - && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) { - if (appWidgetInfo == null) { - getModelWriter().deleteItemFromDatabase(item, - "Removing restored widget: id=" + item.appWidgetId - + " belongs to component " + item.providerName + " user " + item.user - + ", as the provider is null and " + removalReason); - return null; - } - - // If we do not have a valid id, try to bind an id. - if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { - if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) { - // Id has not been allocated yet. Allocate a new id. - item.appWidgetId = mAppWidgetHolder.allocateAppWidgetId(); - item.restoreStatus |= LauncherAppWidgetInfo.FLAG_ID_ALLOCATED; - - // Also try to bind the widget. If the bind fails, the user will be shown - // a click to setup UI, which will ask for the bind permission. - PendingAddWidgetInfo pendingInfo = - new PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer); - pendingInfo.spanX = item.spanX; - pendingInfo.spanY = item.spanY; - pendingInfo.minSpanX = item.minSpanX; - pendingInfo.minSpanY = item.minSpanY; - Bundle options = pendingInfo.getDefaultSizeOptions(this); - - boolean isDirectConfig = - item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG); - if (isDirectConfig && item.bindOptions != null) { - Bundle newOptions = item.bindOptions.getExtras(); - if (options != null) { - newOptions.putAll(options); - } - options = newOptions; - } - boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( - item.appWidgetId, appWidgetInfo, options); - - // We tried to bind once. If we were not able to bind, we would need to - // go through the permission dialog, which means we cannot skip the config - // activity. - item.bindOptions = null; - item.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG; - - // Bind succeeded - if (success) { - // If the widget has a configure activity, it is still needs to set it - // up, otherwise the widget is ready to go. - item.restoreStatus = (appWidgetInfo.configure == null) || isDirectConfig - ? LauncherAppWidgetInfo.RESTORE_COMPLETED - : LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - } - - getModelWriter().updateItemInDatabase(item); - } - } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) - && (appWidgetInfo.configure == null)) { - // The widget was marked as UI not ready, but there is no configure activity to - // update the UI. - item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED; - getModelWriter().updateItemInDatabase(item); - } - else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) - && appWidgetInfo.configure != null) { - if (mAppWidgetManager.isAppWidgetRestored(item.appWidgetId)) { - item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED; - getModelWriter().updateItemInDatabase(item); - } - } - } - - if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { - // Verify that we own the widget - if (appWidgetInfo == null) { - FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId); - getModelWriter().deleteWidgetInfo(item, getAppWidgetHolder(), removalReason); - return null; - } - - item.minSpanX = appWidgetInfo.minSpanX; - item.minSpanY = appWidgetInfo.minSpanY; - view = mAppWidgetHolder.createView(this, item.appWidgetId, appWidgetInfo); - } else if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) - && appWidgetInfo != null) { - mAppWidgetHolder.addPendingView(item.appWidgetId, - new PendingAppWidgetHostView(this, item, mIconCache, false)); - view = mAppWidgetHolder.createView(this, item.appWidgetId, appWidgetInfo); - } else { - view = new PendingAppWidgetHostView(this, item, mIconCache, false); - } - prepareAppWidget(view, item); - } finally { - TraceHelper.INSTANCE.endSection(); - } - - return view; - } - /** * Restores a pending widget. * @@ -2448,7 +2282,7 @@ public class Launcher extends StatefulActivity */ private LauncherAppWidgetInfo completeRestoreAppWidget(int appWidgetId, int finalRestoreFlag) { LauncherAppWidgetHostView view = mWorkspace.getWidgetForAppWidgetId(appWidgetId); - if ((view == null) || !(view instanceof PendingAppWidgetHostView)) { + if (!(view instanceof PendingAppWidgetHostView)) { Log.e(TAG, "Widget update called, when the widget no longer exists."); return null; } @@ -2459,20 +2293,15 @@ public class Launcher extends StatefulActivity info.pendingItemInfo = null; } - if (((PendingAppWidgetHostView) view).isReinflateIfNeeded()) { - view.reInflate(); + PendingAppWidgetHostView pv = (PendingAppWidgetHostView) view; + if (pv.isReinflateIfNeeded()) { + pv.reInflate(); } getModelWriter().updateItemInDatabase(info); return info; } - public void clearPendingExecutor(ViewOnDrawExecutor executor) { - if (mModelCallbacks.getPendingExecutor() == executor) { - mModelCallbacks.setPendingExecutor(null); - } - } - /** * Call back when ModelCallbacks finish binding the Launcher data. */ @@ -2501,9 +2330,9 @@ public class Launcher extends StatefulActivity @Override public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks, - int workspaceItemCount, boolean isBindSync) { - mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, workspaceItemCount, - isBindSync); + RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) { + mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal, + workspaceItemCount, isBindSync); } /** @@ -2607,6 +2436,18 @@ public class Launcher extends StatefulActivity return null; } + /** Convert a {@link View} to {@link Bitmap}. */ + private static Bitmap getBitmapFromView(@Nullable View view) { + if (view == null) { + return null; + } + Bitmap returnedBitmap = + Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(returnedBitmap); + view.draw(canvas); + return returnedBitmap; + } + /** * Returns the first view matching the operator in the given ViewGroups, or null if none. * Forward iteration matters. @@ -2879,14 +2720,13 @@ public class Launcher extends StatefulActivity } private void updateDisallowBack() { - // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding - if (ENABLE_DESKTOP_WINDOWING || DESKTOP_MODE_SUPPORTED) { + if (Flags.enableDesktopWindowingMode()) { // Do not disable back in launcher when prototype behavior is enabled return; } LauncherRootView rv = getRootView(); if (rv != null) { - boolean isSplitSelectionEnabled = isSplitSelectionEnabled(); + boolean isSplitSelectionEnabled = isSplitSelectionActive(); boolean disableBack = getStateManager().getState() == NORMAL && AbstractFloatingView.getTopOpenView(this) == null && !isSplitSelectionEnabled; @@ -2895,13 +2735,13 @@ public class Launcher extends StatefulActivity } /** To be overridden by subclasses */ - public boolean isSplitSelectionEnabled() { + public boolean isSplitSelectionActive() { // Overridden return false; } /** Call to dismiss the intermediary split selection state. */ - public void dismissSplitSelection() { + public void dismissSplitSelection(StatsLogManager.LauncherEvent splitDismissEvent) { // Overridden; move this into ActivityContext if necessary for Taskbar } @@ -3044,7 +2884,7 @@ public class Launcher extends StatefulActivity /** * Call this after onCreate to set or clear overlay. */ - public void setLauncherOverlay(LauncherOverlay overlay) { + public void setLauncherOverlay(LauncherOverlayTouchProxy overlay) { mWorkspace.setLauncherOverlay(overlay); } @@ -3222,6 +3062,11 @@ public class Launcher extends StatefulActivity return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId); } + @Override + public ItemInflater getItemInflater() { + return mItemInflater; + } + /** * Returns the current popup for testing, if any. */ diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 9a19526fa2..60a6be6c9e 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -34,13 +34,10 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.LauncherApps; +import android.content.pm.LauncherApps.ArchiveCompatibilityParams; import android.os.UserHandle; import android.util.Log; -import android.util.SparseArray; -import android.widget.RemoteViews; -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.graphics.IconShape; @@ -48,6 +45,7 @@ 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.model.ModelLauncherCallbacks; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.InstallSessionTracker; @@ -60,6 +58,7 @@ import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.launcher3.util.Themes; +import com.android.launcher3.util.TraceHelper; import com.android.launcher3.widget.custom.CustomWidgetManager; public class LauncherAppState implements SafeCloseable { @@ -75,13 +74,9 @@ public class LauncherAppState implements SafeCloseable { private final LauncherIconProvider mIconProvider; private final IconCache mIconCache; private final InvariantDeviceProfile mInvariantDeviceProfile; - private final RunnableList mOnTerminateCallback = new RunnableList(); + private boolean mIsSafeModeEnabled; - // WORKAROUND: b/269335387 remove this after widget background listener is enabled - /* Array of RemoteViews cached by Launcher process */ - @GuardedBy("itself") - @NonNull - public final SparseArray mCachedRemoteViews = new SparseArray<>(); + private final RunnableList mOnTerminateCallback = new RunnableList(); public static LauncherAppState getInstance(final Context context) { return INSTANCE.get(context); @@ -95,20 +90,31 @@ public class LauncherAppState implements SafeCloseable { return mContext; } + @SuppressWarnings("NewApi") public LauncherAppState(Context context) { this(context, LauncherFiles.APP_ICONS_DB); Log.v(Launcher.TAG, "LauncherAppState initiated"); Preconditions.assertUIThread(); + mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode", + () -> context.getPackageManager().isSafeMode()); mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> { if (modelPropertiesChanged) { refreshAndReloadLauncher(); } }); - mContext.getSystemService(LauncherApps.class).registerCallback(mModel); + ModelLauncherCallbacks callbacks = mModel.newModelCallbacks(); + LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); + launcherApps.registerCallback(callbacks); mOnTerminateCallback.add(() -> - mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel)); + mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks)); + + if (Utilities.enableSupportForArchiving()) { + ArchiveCompatibilityParams params = new ArchiveCompatibilityParams(); + params.setEnableUnarchivalConfirmation(false); + launcherApps.setArchiveCompatibility(params); + } SimpleBroadcastReceiver modelChangeReceiver = new SimpleBroadcastReceiver(mModel::onBroadcastIntent); @@ -234,6 +240,10 @@ public class LauncherAppState implements SafeCloseable { return mInvariantDeviceProfile; } + public boolean isSafeModeEnabled() { + return mIsSafeModeEnabled; + } + /** * Shorthand for {@link #getInvariantDeviceProfile()} */ diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index c81db63b34..99fca62ac1 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURC import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD; +import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE; import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE; import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE; import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing; @@ -28,7 +29,6 @@ import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.content.Context; import android.content.Intent; -import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller; import android.content.pm.ShortcutInfo; import android.os.UserHandle; @@ -43,7 +43,6 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.celllayout.CellPosMapper; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.AddWorkspaceItemsTask; import com.android.launcher3.model.AllAppsList; import com.android.launcher3.model.BaseModelUpdateTask; @@ -55,8 +54,8 @@ import com.android.launcher3.model.LauncherBinder; import com.android.launcher3.model.LoaderTask; import com.android.launcher3.model.ModelDbController; import com.android.launcher3.model.ModelDelegate; +import com.android.launcher3.model.ModelLauncherCallbacks; 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; @@ -89,7 +88,7 @@ import java.util.function.Supplier; * LauncherModel object held in a static. Also provide APIs for updating the database state * for the Launcher. */ -public class LauncherModel extends LauncherApps.Callback implements InstallSessionTracker.Callback { +public class LauncherModel implements InstallSessionTracker.Callback { private static final boolean DEBUG_RECEIVER = false; static final String TAG = "Launcher.Model"; @@ -168,6 +167,10 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi return mModelDbController; } + public ModelLauncherCallbacks newModelCallbacks() { + return new ModelLauncherCallbacks(this::enqueueModelUpdateTask); + } + /** * Adds the provided items to the workspace. */ @@ -186,77 +189,6 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi owner); } - @Override - public void onPackageChanged( - @NonNull final String packageName, @NonNull final UserHandle user) { - int op = PackageUpdatedTask.OP_UPDATE; - enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); - } - - @Override - public void onPackageRemoved( - @NonNull final String packageName, @NonNull final UserHandle user) { - onPackagesRemoved(user, packageName); - } - - public void onPackagesRemoved( - @NonNull final UserHandle user, @NonNull final String... packages) { - int op = PackageUpdatedTask.OP_REMOVE; - FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages)); - enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages)); - } - - @Override - public void onPackageAdded(@NonNull final String packageName, @NonNull final UserHandle user) { - int op = PackageUpdatedTask.OP_ADD; - enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); - } - - @Override - public void onPackagesAvailable(@NonNull final String[] packageNames, - @NonNull final UserHandle user, final boolean replacing) { - enqueueModelUpdateTask( - new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames)); - } - - @Override - public void onPackagesUnavailable(@NonNull final String[] packageNames, - @NonNull final UserHandle user, final boolean replacing) { - if (!replacing) { - enqueueModelUpdateTask(new PackageUpdatedTask( - PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames)); - } - } - - @Override - public void onPackagesSuspended( - @NonNull final String[] packageNames, @NonNull final UserHandle user) { - enqueueModelUpdateTask(new PackageUpdatedTask( - PackageUpdatedTask.OP_SUSPEND, user, packageNames)); - } - - @Override - public void onPackagesUnsuspended( - @NonNull final String[] packageNames, @NonNull final UserHandle user) { - enqueueModelUpdateTask(new PackageUpdatedTask( - PackageUpdatedTask.OP_UNSUSPEND, user, packageNames)); - } - - @Override - public void onPackageLoadingProgressChanged(@NonNull final String packageName, - @NonNull final UserHandle user, final float progress) { - if (Utilities.ATLEAST_S) { - enqueueModelUpdateTask(new PackageIncrementalDownloadUpdatedTask( - packageName, user, progress)); - } - } - - @Override - public void onShortcutsChanged(@NonNull final String packageName, - @NonNull final List shortcuts, @NonNull final UserHandle user) { - enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true)); - } - /** * Called when the icon for an app changes, outside of package event */ @@ -265,7 +197,7 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi @NonNull final UserHandle user) { // Update the icon for the calendar package Context context = mApp.getContext(); - onPackageChanged(packageName, user); + enqueueModelUpdateTask(new PackageUpdatedTask(OP_UPDATE, user, packageName)); List pinnedShortcuts = new ShortcutRequest(context, user) .forPackage(packageName).query(ShortcutRequest.PINNED); @@ -509,17 +441,35 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi @Override public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { + IconCache iconCache = app.getIconCache(); final IntSet removedIds = new IntSet(); + HashSet archivedItemsToCacheRefresh = new HashSet<>(); + HashSet archivedPackagesToCacheRefresh = new HashSet<>(); synchronized (dataModel) { for (ItemInfo info : dataModel.itemsIdMap) { if (info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi() && user.equals(info.user) - && info.getIntent() != null - && TextUtils.equals(packageName, info.getIntent().getPackage())) { - removedIds.add(info.id); + && info.getIntent() != null) { + if (TextUtils.equals(packageName, info.getIntent().getPackage())) { + removedIds.add(info.id); + } + if (((WorkspaceItemInfo) info).isArchived()) { + WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info; + // Remove package cache icon for archived app in case of a session + // failure. + mApp.getIconCache().removeIconsForPkg(packageName, user); + // Refresh icons on the workspace for archived apps. + iconCache.getTitleAndIcon(workspaceItem, + workspaceItem.usingLowResIcon()); + archivedPackagesToCacheRefresh.add(packageName); + archivedItemsToCacheRefresh.add(workspaceItem); + } } } + if (!archivedPackagesToCacheRefresh.isEmpty()) { + apps.updateIconsAndLabels(archivedPackagesToCacheRefresh, user); + } } if (!removedIds.isEmpty()) { @@ -527,6 +477,10 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi ItemInfoMatcher.ofItemIds(removedIds), "removed because install session failed"); } + if (!archivedItemsToCacheRefresh.isEmpty()) { + bindUpdatedWorkspaceItems(archivedItemsToCacheRefresh.stream().toList()); + bindApplicationsIfNeeded(); + } } }); } diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt index 78056e625b..b0a644b269 100644 --- a/src/com/android/launcher3/LauncherPrefs.kt +++ b/src/com/android/launcher3/LauncherPrefs.kt @@ -20,20 +20,15 @@ import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.util.Log +import android.view.ViewConfiguration import androidx.annotation.VisibleForTesting import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY -import com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_DELAY -import com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_END_SCALE_PERCENT -import com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_ITERATIONS -import com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_SCALE_EXPONENT -import com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_START_SCALE_PERCENT -import com.android.launcher3.config.FeatureFlags.LPNH_SLOP_PERCENTAGE -import com.android.launcher3.config.FeatureFlags.LPNH_TIMEOUT_MS import com.android.launcher3.model.DeviceGridState import com.android.launcher3.pm.InstallSessionHelper import com.android.launcher3.provider.RestoreDbTask +import com.android.launcher3.provider.RestoreDbTask.FIRST_LOAD_AFTER_RESTORE_KEY import com.android.launcher3.states.RotationHelper import com.android.launcher3.util.DisplayController import com.android.launcher3.util.MainThreadInitializedObject @@ -302,74 +297,60 @@ class LauncherPrefs(private val encryptedContext: Context) { const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY" @JvmField val ICON_STATE = - nonRestorableItem( - "pref_icon_shape_path", - "", - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem("pref_icon_shape_path", "", EncryptionType.MOVE_TO_DEVICE_PROTECTED) @JvmField val ALL_APPS_OVERVIEW_THRESHOLD = nonRestorableItem( - "pref_all_apps_overview_threshold", + "pref_all_apps_overview_threshold", 180, EncryptionType.MOVE_TO_DEVICE_PROTECTED ) @JvmField val LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE = - nonRestorableItem( - "pref_long_press_nav_handle_slop_percentage", - LPNH_SLOP_PERCENTAGE.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem("LPNH_SLOP_PERCENTAGE", 100, EncryptionType.MOVE_TO_DEVICE_PROTECTED) @JvmField val LONG_PRESS_NAV_HANDLE_TIMEOUT_MS = - nonRestorableItem( - "pref_long_press_nav_handle_timeout_ms", - LPNH_TIMEOUT_MS.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem( + "LPNH_TIMEOUT_MS", + ViewConfiguration.getLongPressTimeout(), + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT = - nonRestorableItem( - "pref_long_press_nav_handle_haptic_hint_start_scale_percent", - LPNH_HAPTIC_HINT_START_SCALE_PERCENT.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem( + "LPNH_HAPTIC_HINT_START_SCALE_PERCENT", + 0, + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT = - nonRestorableItem( - "pref_long_press_nav_handle_haptic_hint_end_scale_percent", - LPNH_HAPTIC_HINT_END_SCALE_PERCENT.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem( + "LPNH_HAPTIC_HINT_END_SCALE_PERCENT", + 100, + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT = - nonRestorableItem( - "pref_long_press_nav_handle_haptic_hint_scale_exponent", - LPNH_HAPTIC_HINT_SCALE_EXPONENT.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem( + "LPNH_HAPTIC_HINT_SCALE_EXPONENT", + 1, + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS = - nonRestorableItem( - "pref_long_press_nav_handle_haptic_hint_iterations", - LPNH_HAPTIC_HINT_ITERATIONS.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem( + "LPNH_HAPTIC_HINT_ITERATIONS", + 50, + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY = - nonRestorableItem( - "pref_long_press_nav_handle_haptic_hint_delay", - LPNH_HAPTIC_HINT_DELAY.get(), - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem("LPNH_HAPTIC_HINT_DELAY", 0, EncryptionType.MOVE_TO_DEVICE_PROTECTED) @JvmField val PRIVATE_SPACE_APPS = - nonRestorableItem( - "pref_private_space_apps", - 0, - EncryptionType.MOVE_TO_DEVICE_PROTECTED - ) + nonRestorableItem("pref_private_space_apps", 0, EncryptionType.MOVE_TO_DEVICE_PROTECTED) + @JvmField val ENABLE_TWOLINE_ALLAPPS_TOGGLE = + backedUpItem("pref_enable_two_line_toggle", false) @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED) @@ -417,6 +398,13 @@ class LauncherPrefs(private val encryptedContext: Context) { InvariantDeviceProfile.TYPE_PHONE, EncryptionType.MOVE_TO_DEVICE_PROTECTED ) + @JvmField + val IS_FIRST_LOAD_AFTER_RESTORE = + nonRestorableItem( + FIRST_LOAD_AFTER_RESTORE_KEY, + false, + EncryptionType.MOVE_TO_DEVICE_PROTECTED + ) @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") @JvmField diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java index 1592154c3a..7176733786 100644 --- a/src/com/android/launcher3/LauncherRootView.java +++ b/src/com/android/launcher3/LauncherRootView.java @@ -2,11 +2,9 @@ package com.android.launcher3; import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; -import android.os.Build; import android.util.AttributeSet; import android.view.ViewDebug; import android.view.WindowInsets; @@ -112,15 +110,13 @@ public class LauncherRootView extends InsettableFrameLayout { mSysUiScrim.setSize(r - l, b - t); } - @TargetApi(Build.VERSION_CODES.Q) public void setForceHideBackArrow(boolean forceHideBackArrow) { this.mForceHideBackArrow = forceHideBackArrow; setDisallowBackGesture(mDisallowBackGesture); } - @TargetApi(Build.VERSION_CODES.Q) public void setDisallowBackGesture(boolean disallowBackGesture) { - if (!Utilities.ATLEAST_Q || SEPARATE_RECENTS_ACTIVITY.get()) { + if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } mDisallowBackGesture = disallowBackGesture; diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java index d2ea7cc3f0..6e66c1448d 100644 --- a/src/com/android/launcher3/LauncherState.java +++ b/src/com/android/launcher3/LauncherState.java @@ -17,6 +17,7 @@ package com.android.launcher3; import static com.android.app.animation.Interpolators.ACCELERATE_2; import static com.android.app.animation.Interpolators.DECELERATE_2; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; 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.shared.TestProtocol.ALL_APPS_STATE_ORDINAL; @@ -427,10 +428,19 @@ public abstract class LauncherState implements BaseState { if (this != NORMAL) { StateManager lsm = launcher.getStateManager(); LauncherState lastState = lsm.getLastState(); - lsm.goToState(lastState); + lsm.goToState(lastState, forEndCallback(this::onBackPressCompleted)); } } + /** + * To be called if back press is completed in a launcher state. + * + * @param success whether back press animation was successful or canceled. + */ + protected void onBackPressCompleted(boolean success) { + // Do nothing. To be overridden by child class. + } + /** * Find {@link StateManager} and target {@link LauncherState} to handle back progress in * predictive back gesture. diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt index 51729992d4..9b65a310eb 100644 --- a/src/com/android/launcher3/ModelCallbacks.kt +++ b/src/com/android/launcher3/ModelCallbacks.kt @@ -3,7 +3,6 @@ package com.android.launcher3 import android.annotation.TargetApi import android.os.Build import android.os.Trace -import android.view.ViewTreeObserver.OnDrawListener import androidx.annotation.UiThread import com.android.launcher3.LauncherConstants.TraceEvents import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID @@ -18,7 +17,6 @@ import com.android.launcher3.model.data.LauncherAppWidgetInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.popup.PopupContainerWithArrow import com.android.launcher3.util.ComponentKey -import com.android.launcher3.util.Executors import com.android.launcher3.util.IntArray as LIntArray import com.android.launcher3.util.IntSet as LIntSet import com.android.launcher3.util.PackageUserKey @@ -74,14 +72,19 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { override fun onInitialBindComplete( boundPages: LIntSet, pendingTasks: RunnableList, + onCompleteSignal: RunnableList, workspaceItemCount: Int, isBindSync: Boolean ) { + if (Utilities.ATLEAST_S) { + Trace.endAsyncSection( + TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME, + TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE + ) + } synchronouslyBoundPages = boundPages pagesToBindSynchronously = LIntSet() clearPendingBinds() - val executor = ViewOnDrawExecutor(pendingTasks) - pendingExecutor = executor if (!launcher.isInState(LauncherState.ALL_APPS)) { launcher.appsView.appsStore.enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW) pendingTasks.add { @@ -90,24 +93,22 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { ) } } - executor.onLoadAnimationCompleted() - executor.attachTo(launcher) - if (Utilities.ATLEAST_S) { - Trace.endAsyncSection( - TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME, - TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE - ) - } - launcher.bindComplete(workspaceItemCount, isBindSync) - launcher.rootView.viewTreeObserver.addOnDrawListener( - object : OnDrawListener { - override fun onDraw() { - Executors.MAIN_EXECUTOR.handler.postAtFrontOfQueue { - launcher.rootView.getViewTreeObserver().removeOnDrawListener(this) - } + val executor = + ViewOnDrawExecutor(pendingTasks) { + if (pendingExecutor == it) { + pendingExecutor = null } } - ) + pendingExecutor = executor + + if (Flags.enableWorkspaceInflation()) { + // Finish the executor as soon as the pending inflation is completed + onCompleteSignal.add(executor::markCompleted) + } else { + // Pending executor is already completed, wait until first draw to run the tasks + executor.attachTo(launcher) + } + launcher.bindComplete(workspaceItemCount, isBindSync) } /** @@ -139,7 +140,7 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { launcher.viewCache.setCacheSize(R.layout.folder_page, 2) TraceHelper.INSTANCE.endSection() launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true) - launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels) + launcher.workspace.pageIndicator.setPauseScroll(/*pause=*/ false, deviceProfile.isTwoPanels) } /** @@ -178,7 +179,10 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { val hadWorkApps = launcher.appsView.shouldShowTabs() launcher.appsView.appsStore.setApps(apps, flags, packageUserKeytoUidMap) PopupContainerWithArrow.dismissInvalidPopup(launcher) - if (hadWorkApps != launcher.appsView.shouldShowTabs()) { + if ( + hadWorkApps != launcher.appsView.shouldShowTabs() && + launcher.stateManager.state == LauncherState.ALL_APPS + ) { launcher.stateManager.goToState(LauncherState.NORMAL) } } @@ -303,8 +307,8 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { } override fun bindScreens(orderedScreenIds: LIntArray) { - launcher.workspace.pageIndicator.setAreScreensBinding( - true, + launcher.workspace.pageIndicator.setPauseScroll( + /*pause=*/ true, launcher.deviceProfile.isTwoPanels ) val firstScreenPosition = 0 @@ -413,4 +417,6 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { } fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled + + override fun getItemInflater() = launcher.itemInflater } diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index f355ae7128..1fede56442 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -118,7 +118,8 @@ public abstract class PagedView extends ViewGrou private float mTotalMotion; // Used in special cases where the fling checks can be relaxed for an intentional gesture private boolean mAllowEasyFling; - protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT; + private PagedOrientationHandler mOrientationHandler = + PagedOrientationHandler.DEFAULT; private final ArrayList mOnPageScrollsInitializedCallbacks = new ArrayList<>(); @@ -231,6 +232,14 @@ public abstract class PagedView extends ViewGrou return getChildAt(index); } + protected PagedOrientationHandler getPagedOrientationHandler() { + return mOrientationHandler; + } + + protected void setOrientationHandler(PagedOrientationHandler orientationHandler) { + this.mOrientationHandler = orientationHandler; + } + /** * Updates the scroll of the current page immediately to its final scroll position. We use this * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of @@ -628,6 +637,11 @@ public abstract class PagedView extends ViewGrou mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity); mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity); mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration); + onVelocityValuesUpdated(); + } + + protected void onVelocityValuesUpdated() { + // Overridden in RecentsView } @Override @@ -1126,7 +1140,7 @@ public abstract class PagedView extends ViewGrou mEdgeGlowLeft.onPullDistance(0f, 1f - displacement); } if (!mEdgeGlowRight.isFinished()) { - mEdgeGlowRight.onPullDistance(0f, displacement); + mEdgeGlowRight.onPullDistance(0f, displacement, ev); } } @@ -1306,10 +1320,10 @@ public abstract class PagedView extends ViewGrou int consumed = 0; if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) { consumed = Math.round(size * - mEdgeGlowRight.onPullDistance(delta / size, displacement)); + mEdgeGlowRight.onPullDistance(delta / size, displacement, ev)); } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) { consumed = Math.round(-size * - mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement)); + mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement, ev)); } delta -= consumed; } @@ -1327,14 +1341,14 @@ public abstract class PagedView extends ViewGrou final float pulledToX = oldScroll + delta; if (pulledToX < mMinScroll) { - mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement); + mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement, ev); if (!mEdgeGlowRight.isFinished()) { - mEdgeGlowRight.onRelease(); + mEdgeGlowRight.onRelease(ev); } } else if (pulledToX > mMaxScroll) { - mEdgeGlowRight.onPullDistance(delta / size, displacement); + mEdgeGlowRight.onPullDistance(delta / size, displacement, ev); if (!mEdgeGlowLeft.isFinished()) { - mEdgeGlowLeft.onRelease(); + mEdgeGlowLeft.onRelease(ev); } } @@ -1342,7 +1356,6 @@ public abstract class PagedView extends ViewGrou postInvalidateOnAnimation(); } } - } else { awakenScrollBars(); } @@ -1442,10 +1455,11 @@ public abstract class PagedView extends ViewGrou } invalidate(); } + mEdgeGlowLeft.onFlingVelocity(velocity); + mEdgeGlowRight.onFlingVelocity(velocity); } - - mEdgeGlowLeft.onRelease(); - mEdgeGlowRight.onRelease(); + mEdgeGlowLeft.onRelease(ev); + mEdgeGlowRight.onRelease(ev); // End any intermediate reordering states resetTouchState(); break; @@ -1454,8 +1468,8 @@ public abstract class PagedView extends ViewGrou if (mIsBeingDragged) { runOnPageScrollsInitialized(this::snapToDestination); } - mEdgeGlowLeft.onRelease(); - mEdgeGlowRight.onRelease(); + mEdgeGlowLeft.onRelease(ev); + mEdgeGlowRight.onRelease(ev); resetTouchState(); break; @@ -1573,7 +1587,7 @@ public abstract class PagedView extends ViewGrou @Override public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); - if (!shouldHandleRequestChildFocus()) { + if (!shouldHandleRequestChildFocus(child)) { return; } // In case the device is controlled by a controller, mCurrentPage isn't updated properly @@ -1589,7 +1603,7 @@ public abstract class PagedView extends ViewGrou } } - protected boolean shouldHandleRequestChildFocus() { + protected boolean shouldHandleRequestChildFocus(View child) { return true; } @@ -1643,7 +1657,7 @@ public abstract class PagedView extends ViewGrou } protected void snapToDestination() { - snapToPage(getDestinationPage(), mPageSnapAnimationDuration); + snapToPage(getDestinationPage(), getSnapAnimationDuration()); } // We want the duration of the page snap animation to be influenced by the distance that @@ -1667,7 +1681,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, mPageSnapAnimationDuration); + return snapToPage(whichPage, getSnapAnimationDuration()); } // Here we compute a "distance" that will be used in the computation of the overall @@ -1689,12 +1703,16 @@ public abstract class PagedView extends ViewGrou return snapToPage(whichPage, delta, duration); } + protected int getSnapAnimationDuration() { + return mPageSnapAnimationDuration; + } + public boolean snapToPage(int whichPage) { - return snapToPage(whichPage, mPageSnapAnimationDuration); + return snapToPage(whichPage, getSnapAnimationDuration()); } public boolean snapToPageImmediately(int whichPage) { - return snapToPage(whichPage, mPageSnapAnimationDuration, true); + return snapToPage(whichPage, getSnapAnimationDuration(), true); } public boolean snapToPage(int whichPage, int duration) { diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index 2dd610cbf9..1362586c9e 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -34,6 +34,8 @@ import android.util.Log; import android.view.View; import android.widget.Toast; +import androidx.annotation.Nullable; + import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.logging.FileLog; @@ -43,6 +45,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; @@ -155,6 +158,9 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList } return INVALID; } else if (info.isPredictedItem()) { + if (Flags.enableShortcutDontSuggestApp()) { + return INVALID; + } return DISMISS_PREDICTION; } @@ -173,6 +179,10 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList if (uninstallDisabled) { return INVALID; } + if (Flags.enablePrivateSpace() && UserCache.getInstance(getContext()).getUserInfo( + info.user).isPrivate()) { + return INVALID; + } if (info instanceof ItemInfoWithIcon) { ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info; @@ -181,7 +191,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList return INVALID; } } - if (getUninstallTarget(info) == null) { + if (getUninstallTarget(getContext(), info) == null) { return INVALID; } return UNINSTALL; @@ -190,7 +200,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList /** * @return the component name that should be uninstalled or null. */ - private ComponentName getUninstallTarget(ItemInfo item) { + public static ComponentName getUninstallTarget(Context context, ItemInfo item) { Intent intent = null; UserHandle user = null; if (item != null && @@ -199,7 +209,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList user = item.user; } if (intent != null) { - LauncherActivityInfo info = getContext().getSystemService(LauncherApps.class) + LauncherActivityInfo info = context.getSystemService(LauncherApps.class) .resolveActivity(intent, user); if (info != null && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) { @@ -277,32 +287,41 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) { CharSequence announcement = getContext().getString(R.string.item_removed); mDropTargetHandler - .dismissPrediction(announcement, () -> {}, () -> { - mStatsLogManager.logger() - .withInstanceId(instanceId) - .withItemInfo(info) - .log(LAUNCHER_DISMISS_PREDICTION_UNDO); - }); + .dismissPrediction(announcement, () -> { + }, () -> { + mStatsLogManager.logger() + .withInstanceId(instanceId) + .withItemInfo(info) + .log(LAUNCHER_DISMISS_PREDICTION_UNDO); + }); } return null; } - ComponentName cn = getUninstallTarget(info); + return performUninstall(getContext(), getUninstallTarget(getContext(), info), info); + } + + /** + * Performs uninstall and returns the target component for the {@link ItemInfo} or null if + * the uninstall was not performed. + */ + public static ComponentName performUninstall(Context context, @Nullable ComponentName cn, + ItemInfo info) { if (cn == null) { // System applications cannot be installed. For now, show a toast explaining that. // We may give them the option of disabling apps this way. Toast.makeText( - getContext(), + context, R.string.uninstall_system_app_text, Toast.LENGTH_SHORT - ).show(); + ).show(); return null; } try { - Intent i = Intent.parseUri(getContext().getString(R.string.delete_package_intent), 0) + Intent i = Intent.parseUri(context.getString(R.string.delete_package_intent), 0) .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName())) .putExtra(Intent.EXTRA_USER, info.user); - getContext().startActivity(i); + context.startActivity(i); FileLog.d(TAG, "start uninstall activity " + cn.getPackageName()); return cn; } catch (URISyntaxException e) { diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java index d460ba8668..6168e41464 100644 --- a/src/com/android/launcher3/SessionCommitReceiver.java +++ b/src/com/android/launcher3/SessionCommitReceiver.java @@ -30,6 +30,7 @@ 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.pm.UserCache; import com.android.launcher3.util.Executors; import java.util.Locale; @@ -51,13 +52,13 @@ public class SessionCommitReceiver extends BroadcastReceiver { @WorkerThread private static void processIntent(Context context, Intent intent) { - if (!isEnabled(context)) { + UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); + if (!isEnabled(context, user)) { // User has decided to not add icons on homescreen. return; } SessionInfo info = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); - UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); if (!PackageInstaller.ACTION_SESSION_COMMITTED.equals(intent.getAction()) || info == null || user == null) { // Invalid intent. @@ -92,7 +93,17 @@ public class SessionCommitReceiver extends BroadcastReceiver { .queueItem(info.getAppPackageName(), user); } - public static boolean isEnabled(Context context) { + /** + * Returns whether adding Installed App Icons to home screen is allowed or not. + * Not allowed when: + * - User belongs to {@link com.android.launcher3.util.UserIconInfo.TYPE_PRIVATE} or + * - Home Settings preference to add App Icons on Home Screen is set as disabled + */ + public static boolean isEnabled(Context context, UserHandle user) { + if (Flags.privateSpaceRestrictItemDrag() && user != null + && UserCache.getInstance(context).getUserInfo(user).isPrivate()) { + return false; + } return LauncherPrefs.getPrefs(context).getBoolean(ADD_ICON_PREFERENCE_KEY, true); } } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index e0f6101056..d44438f5bc 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -121,15 +121,6 @@ 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; - @ChecksSdkIntAtLeast(api = VERSION_CODES.S) public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; @@ -839,4 +830,10 @@ public final class Utilities { // No-Op } } + + /** Encapsulates two flag checks into a single one. */ + public static boolean enableSupportForArchiving() { + return Flags.enableSupportForArchiving() + || getSystemProperty("pm.archiving.enabled", "false").equals("true"); + } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index be4168db3c..ca34dd1a0a 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -73,7 +73,6 @@ import com.android.app.animation.Interpolators; import com.android.launcher3.accessibility.AccessibleDragListenerAdapter; import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.celllayout.CellInfo; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.celllayout.CellPosMapper; @@ -99,7 +98,6 @@ import com.android.launcher3.logging.StatsLogManager.LauncherEvent; 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.pageindicators.PageIndicator; import com.android.launcher3.statemanager.StateManager; @@ -127,8 +125,8 @@ import com.android.launcher3.widget.PendingAppWidgetHostView; import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener; import com.android.launcher3.widget.util.WidgetSizes; -import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay; import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks; +import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy; import java.util.ArrayList; import java.util.Iterator; @@ -145,7 +143,7 @@ import java.util.stream.Collectors; * @param Class that extends View and PageIndicator */ public class Workspace extends PagedView - implements DropTarget, DragSource, View.OnTouchListener, + implements DropTarget, DragSource, View.OnTouchListener, CellLayoutContainer, DragController.DragListener, Insettable, StateHandler, WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks { @@ -513,11 +511,6 @@ public class Workspace extends PagedView return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels; } - @Override - public int getPanelCount() { - return isTwoPanelEnabled() ? 2 : super.getPanelCount(); - } - public void deferRemoveExtraEmptyScreen() { mDeferRemoveExtraEmptyScreen = true; } @@ -685,6 +678,7 @@ public class Workspace extends PagedView newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate( R.layout.workspace_screen, this, false /* attachToRoot */); } + newScreen.setCellLayoutContainer(this); mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); @@ -951,7 +945,8 @@ public class Workspace extends PagedView return mWorkspaceScreens.get(screenId); } - public int getIdForScreen(CellLayout layout) { + @Override + public int getCellLayoutId(CellLayout layout) { int index = mWorkspaceScreens.indexOfValue(layout); if (index != -1) { return mWorkspaceScreens.keyAt(index); @@ -963,6 +958,16 @@ public class Workspace extends PagedView return indexOfChild(mWorkspaceScreens.get(screenId)); } + @Override + public int getCellLayoutIndex(CellLayout cellLayout) { + return indexOfChild(mWorkspaceScreens.get(getCellLayoutId(cellLayout))); + } + + @Override + public int getPanelCount() { + return isTwoPanelEnabled() ? 2 : super.getPanelCount(); + } + public IntSet getCurrentPageScreenIds() { return IntSet.wrap(getScreenIdForPageIndex(getCurrentPage())); } @@ -1003,7 +1008,7 @@ public class Workspace extends PagedView if (!isTwoPanelEnabled()) { return null; } - int screenId = getIdForScreen(cellLayout); + int screenId = getCellLayoutId(cellLayout); if (screenId == -1) { return null; } @@ -1232,7 +1237,7 @@ public class Workspace extends PagedView mLauncher.onPageEndTransition(); } - public void setLauncherOverlay(LauncherOverlay overlay) { + public void setLauncherOverlay(LauncherOverlayTouchProxy overlay) { final EdgeEffectCompat newEffect; if (overlay == null) { newEffect = new EdgeEffectCompat(getContext()); @@ -1631,9 +1636,15 @@ public class Workspace extends PagedView mDragController.addDragListener( new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) { @Override - protected void enableAccessibleDrag(boolean enable) { - super.enableAccessibleDrag(enable); + protected void enableAccessibleDrag(boolean enable, + @Nullable DragObject dragObject) { + super.enableAccessibleDrag(enable, dragObject); setEnableForLayout(mLauncher.getHotseat(), enable); + if (enable && dragObject != null + && dragObject.dragInfo instanceof LauncherAppWidgetInfo) { + mLauncher.getHotseat().setImportantForAccessibility( + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } } }); } @@ -1828,7 +1839,7 @@ public class Workspace extends PagedView } } - int screenId = getIdForScreen(dropTargetLayout); + int screenId = getCellLayoutId(dropTargetLayout); if (Workspace.EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) { commitExtraEmptyScreens(); } @@ -1911,7 +1922,7 @@ public class Workspace extends PagedView if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false; mCreateUserFolderOnDrop = false; - final int screenId = getIdForScreen(target); + final int screenId = getCellLayoutId(target); boolean aboveShortcut = (v.getTag() instanceof WorkspaceItemInfo); boolean willBecomeShortcut = (newView.getTag() instanceof WorkspaceItemInfo); @@ -2012,7 +2023,7 @@ public class Workspace extends PagedView LauncherSettings.Favorites.CONTAINER_HOTSEAT : LauncherSettings.Favorites.CONTAINER_DESKTOP; int screenId = (mTargetCell[0] < 0) ? - mDragInfo.screenId : getIdForScreen(dropTargetLayout); + mDragInfo.screenId : getCellLayoutId(dropTargetLayout); int spanX = mDragInfo != null ? mDragInfo.spanX : 1; int spanY = mDragInfo != null ? mDragInfo.spanY : 1; // First we find the cell nearest to point at which the item is @@ -2215,8 +2226,7 @@ public class Workspace extends PagedView private Runnable getWidgetResizeFrameRunnable(DragOptions options, LauncherAppWidgetHostView hostView, CellLayout cellLayout) { AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo(); - if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE - && !options.isAccessibleDrag) { + if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { return () -> { if (!isPageInTransition()) { AppWidgetResizeFrame.showForWidget(hostView, cellLayout); @@ -2339,10 +2349,6 @@ public class Workspace extends PagedView } } - public CellLayout getCurrentDragOverlappingLayout() { - return mDragOverlappingLayout; - } - void setCurrentDropOverCell(int x, int y) { if (x != mDragOverX || y != mDragOverY) { mDragOverX = x; @@ -2772,7 +2778,7 @@ public class Workspace extends PagedView final int container = mLauncher.isHotseatLayout(cellLayout) ? LauncherSettings.Favorites.CONTAINER_HOTSEAT : LauncherSettings.Favorites.CONTAINER_DESKTOP; - final int screenId = getIdForScreen(cellLayout); + final int screenId = getCellLayoutId(cellLayout); if (!mLauncher.isHotseatLayout(cellLayout) && screenId != getScreenIdForPageIndex(mCurrentPage) && !mLauncher.isInState(SPRING_LOADED) @@ -2854,36 +2860,9 @@ public class Workspace extends PagedView } else { // This is for other drag/drop cases, like dragging from All Apps mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); - View view; - - switch (info.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: - case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION: - if (info instanceof WorkspaceItemFactory) { - // Came from all apps -- make a copy - info = ((WorkspaceItemFactory) info).makeWorkspaceItem(mLauncher); - d.dragInfo = info; - } - 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); - break; - case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: - view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout, - (FolderInfo) info); - break; - case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: - view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, mLauncher, cellLayout, - (FolderInfo) info); - break; - default: - throw new IllegalStateException("Unknown item type: " + info.itemType); - } + View view = mLauncher.getItemInflater() + .inflateItem(info, mLauncher.getModelWriter(), cellLayout); + d.dragInfo = info = (ItemInfo) view.getTag(); // First we find the cell nearest to point at which the item is // dropped, without any consideration to whether there is an item there. @@ -3008,7 +2987,7 @@ public class Workspace extends PagedView } public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView, - final Runnable onCompleteRunnable, int animationType, final View finalView, + final Runnable onCompleteRunnable, int animationType, @Nullable final View finalView, boolean external) { int[] finalPos = new int[2]; float scaleXY[] = new float[2]; @@ -3513,14 +3492,15 @@ public class Workspace extends PagedView @Override protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - return getPageDescription(page); + int pageIndex = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + return getPageDescription(pageIndex); } /** * @param page page index. * @return Description of the page at the given page index. */ + @Override public String getPageDescription(int page) { int nScreens = getChildCount(); int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID); diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java index 0d7df2b44d..79b81871cc 100644 --- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java +++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java @@ -20,6 +20,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.OnHierarchyChangeListener; +import androidx.annotation.Nullable; + import com.android.launcher3.CellLayout; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Launcher; @@ -50,13 +52,13 @@ public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyC @Override public void onDragStart(DragObject dragObject, DragOptions options) { mViewGroup.setOnHierarchyChangeListener(this); - enableAccessibleDrag(true); + enableAccessibleDrag(true, dragObject); } @Override public void onDragEnd() { mViewGroup.setOnHierarchyChangeListener(null); - enableAccessibleDrag(false); + enableAccessibleDrag(false, null); Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this); } @@ -75,7 +77,7 @@ public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyC } } - protected void enableAccessibleDrag(boolean enable) { + protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) { for (int i = 0; i < mViewGroup.getChildCount(); i++) { setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable); } diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 758bffbeb8..e861d38733 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -5,18 +5,22 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBIL import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; +import android.animation.AnimatorSet; import android.appwidget.AppWidgetProviderInfo; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.os.Handler; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; import com.android.launcher3.BubbleTextView; import com.android.launcher3.ButtonDropTarget; @@ -32,12 +36,12 @@ 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; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.touch.ItemLongClickListener; @@ -70,7 +74,6 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate workspace = mContext.getWorkspace(); + workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); mContext.getModelWriter().addItemToDatabase(fi, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); fi.contents.forEach(member -> { mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1); }); - mContext.bindItems(Collections.singletonList(fi), true, accessibility); + bindItem(fi, accessibility); } })); return true; } + + private void bindItem(ItemInfo item, boolean focusForAccessibility) { + View view = mContext.getItemInflater().inflateItem(item, mContext.getModelWriter()); + if (view == null) { + return; + } + AnimatorSet anim = null; + if (focusForAccessibility) { + anim = new AnimatorSet(); + anim.addListener(forEndCallback( + () -> view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED))); + } + mContext.bindInflatedItems(Collections.singletonList(Pair.create(item, view)), anim); + } + /** * Functionality to move the item {@link ItemInfo} to the workspace * @param item item to be moved diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index fb847ec9ae..d115f9f8bc 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -19,7 +19,6 @@ package com.android.launcher3.accessibility; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; -import android.view.KeyEvent; import android.view.View; import com.android.launcher3.AbstractFloatingView; @@ -28,7 +27,6 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.notification.NotificationMainView; import com.android.launcher3.shortcuts.DeepShortcutView; import java.util.Collections; @@ -40,22 +38,14 @@ import java.util.List; */ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDelegate { - private static final int DISMISS_NOTIFICATION = R.id.action_dismiss_notification; - public ShortcutMenuAccessibilityDelegate(Launcher launcher) { super(launcher); - mActions.put(DISMISS_NOTIFICATION, new LauncherAction(DISMISS_NOTIFICATION, - R.string.action_dismiss_notification, KeyEvent.KEYCODE_X)); } @Override protected void getSupportedActions(View host, ItemInfo item, List out) { if ((host.getParent() instanceof DeepShortcutView)) { out.add(mActions.get(ADD_TO_WORKSPACE)); - } else if (host instanceof NotificationMainView) { - if (((NotificationMainView) host).canChildBeDismissed()) { - out.add(mActions.get(DISMISS_NOTIFICATION)); - } } } @@ -80,13 +70,6 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele announceConfirmation(R.string.item_added_to_workspace); })); return true; - } else if (action == DISMISS_NOTIFICATION) { - if (!(host instanceof NotificationMainView)) { - return false; - } - ((NotificationMainView) host).onChildDismissed(); - announceConfirmation(R.string.notification_dismissed); - return true; } return false; } diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 7f1d216c95..6acfcd0f11 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -80,6 +80,7 @@ import com.android.launcher3.allapps.search.AllAppsSearchUiDelegate; import com.android.launcher3.allapps.search.SearchAdapterProvider; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.keyboard.FocusedItemDecorator; +import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.UserCache; @@ -126,6 +127,7 @@ public class ActivityAllAppsContainerView public static final float PULL_MULTIPLIER = .02f; public static final float FLING_VELOCITY_MULTIPLIER = 1200f; protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; + private static final int SCROLL_TO_BOTTOM_DURATION = 500; private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300; // Render the header protection at all times to debug clipping issues. private static final boolean DEBUG_HEADER_PROTECTION = false; @@ -260,7 +262,7 @@ public class ActivityAllAppsContainerView mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider(); if (Flags.enablePrivateSpace()) { mPrivateSpaceHeaderViewController = - new PrivateSpaceHeaderViewController(mPrivateProfileManager); + new PrivateSpaceHeaderViewController(this, mPrivateProfileManager); } mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN, @@ -515,7 +517,7 @@ public class ActivityAllAppsContainerView // Switch to the main tab switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN); // Scroll to bottom - getActiveRecyclerView().scrollToBottomWithMotion(); + getActiveRecyclerView().scrollToBottomWithMotion(SCROLL_TO_BOTTOM_DURATION); }); } @@ -980,6 +982,11 @@ public class ActivityAllAppsContainerView return mWorkManager; } + /** Returns whether Private Profile has been setup. */ + public boolean hasPrivateProfile() { + return mHasPrivateApps; + } + @Override public void onDeviceProfileChanged(DeviceProfile dp) { for (AdapterHolder holder : mAH) { @@ -1151,13 +1158,16 @@ public class ActivityAllAppsContainerView applyAdapterSideAndBottomPaddings(grid); MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); - mlp.leftMargin = insets.left; - mlp.rightMargin = insets.right; + // Ignore left/right insets on tablet because we are already centered in-screen. + if (grid.isTablet) { + mlp.leftMargin = mlp.rightMargin = 0; + } else { + mlp.leftMargin = insets.left; + mlp.rightMargin = insets.right; + } setLayoutParams(mlp); - if (grid.isVerticalBarLayout() && !FeatureFlags.enableResponsiveWorkspace()) { - setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); - } else { + if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) { int topPadding = grid.allAppsPadding.top; if (isSearchBarFloating() && !grid.isTablet) { topPadding += getResources().getDimensionPixelSize( @@ -1314,6 +1324,10 @@ public class ActivityAllAppsContainerView : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); } + public PrivateProfileManager getPrivateProfileManager() { + return mPrivateProfileManager; + } + /** * Adds an update listener to animator that adds springs to the animation. */ @@ -1522,7 +1536,11 @@ public class ActivityAllAppsContainerView // No animations will occur when changes occur to the items in this RecyclerView. mRecyclerView.setItemAnimator(null); onInitializeRecyclerView(mRecyclerView); - FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); + // Use ViewGroupFocusHelper for SearchRecyclerView to draw focus outline for the + // buttons in the view (e.g. query builder button and setting button) + FocusedItemDecorator focusedItemDecorator = isSearch() ? new FocusedItemDecorator( + new ViewGroupFocusHelper(mRecyclerView)) : new FocusedItemDecorator( + mRecyclerView); mRecyclerView.addItemDecoration(focusedItemDecorator); mOnFocusChangeListener = focusedItemDecorator.getFocusListener(); mAdapter.setIconFocusListener(mOnFocusChangeListener); diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index 7067fa225b..911612ff19 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -71,7 +71,7 @@ public class AllAppsFastScrollHelper { @Override protected int getVerticalSnapPreference() { - return SNAP_TO_START; + return SNAP_TO_ANY; } @Override diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java index 051cf50ccc..9623709348 100644 --- a/src/com/android/launcher3/allapps/AllAppsStore.java +++ b/src/com/android/launcher3/allapps/AllAppsStore.java @@ -40,6 +40,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -93,11 +94,14 @@ public class AllAppsStore { * Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for * the current set of apps. * - *

Note that shouldPreinflate param should be set to {@code false} for taskbar, because this - * method is too late to preinflate all apps, as user will open all apps in the same frame. + *

Note that shouldPreinflate param should be set to {@code false} for taskbar, because + * this method is too late to preinflate all apps, as user will open all apps in the frame + * + *

Param: apps are required to be sorted using the comparator COMPONENT_KEY_COMPARATOR + * in order to enable binary search on the mApps store */ public void setApps(@Nullable AppInfo[] apps, int flags, Map map, - boolean shouldPreinflate) { + boolean shouldPreinflate) { mApps = apps == null ? EMPTY_ARRAY : apps; mModelFlags = flags; notifyUpdate(); @@ -135,12 +139,22 @@ public class AllAppsStore { /** * Returns {@link AppInfo} if any apps matches with provided {@link ComponentKey}, otherwise * null. + * + * Uses {@link AppInfo#COMPONENT_KEY_COMPARATOR} as a default comparator. */ @Nullable public AppInfo getApp(ComponentKey key) { + return getApp(key, COMPONENT_KEY_COMPARATOR); + } + + /** + * Generic version of {@link #getApp(ComponentKey)} that allows comparator to be specified. + */ + @Nullable + public AppInfo getApp(ComponentKey key, Comparator comparator) { mTempInfo.componentName = key.componentName; mTempInfo.user = key.user; - int index = Arrays.binarySearch(mApps, mTempInfo, COMPONENT_KEY_COMPARATOR); + int index = Arrays.binarySearch(mApps, mTempInfo, comparator); return index < 0 ? null : mApps[index]; } diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 03ac9dfa26..e2c5795358 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -342,7 +342,7 @@ public class AllAppsTransitionController }); } - if(FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.userControlled + if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled() && Utilities.ATLEAST_S) { if (toState == ALL_APPS) { builder.addOnFrameListener( @@ -367,7 +367,7 @@ public class AllAppsTransitionController // need to decide depending on the release velocity Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS, - config.userControlled ? LINEAR : DECELERATE_1_7); + config.isUserControlled() ? LINEAR : DECELERATE_1_7); Animator anim = createSpringAnimation(mProgress, targetProgress); anim.setInterpolator(verticalProgressInterpolator); builder.add(anim); diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 17827918b8..fba7537dac 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -22,6 +22,7 @@ import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; import android.content.Context; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.DiffUtil; import com.android.launcher3.Flags; @@ -34,6 +35,7 @@ import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; 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; @@ -268,10 +270,10 @@ public class AlphabeticalAppsList implement addApps = mWorkProviderManager.shouldShowWorkApps(); } if (addApps) { - addAppsWithSections(mApps, position); + position = addAppsWithSections(mApps, position); } if (Flags.enablePrivateSpace()) { - addPrivateSpaceItems(position); + position = addPrivateSpaceItems(position); } } mAccessibilityResultsCount = (int) mAdapterItems.stream() @@ -286,7 +288,8 @@ public class AlphabeticalAppsList implement for (AdapterItem item : mAdapterItems) { item.rowIndex = 0; if (BaseAllAppsAdapter.isDividerViewType(item.viewType) - || BaseAllAppsAdapter.isPrivateSpaceHeaderView(item.viewType)) { + || BaseAllAppsAdapter.isPrivateSpaceHeaderView(item.viewType) + || BaseAllAppsAdapter.isPrivateSpaceSysAppsDividerView(item.viewType)) { numAppsInSection = 0; } else if (BaseAllAppsAdapter.isIconViewType(item.viewType)) { if (numAppsInSection % mNumAppsPerRowAllApps == 0) { @@ -308,12 +311,12 @@ public class AlphabeticalAppsList implement } } - void addPrivateSpaceItems(int position) { + int addPrivateSpaceItems(int position) { if (mPrivateProviderManager != null && !mPrivateProviderManager.isPrivateSpaceHidden() && !mPrivateApps.isEmpty()) { // Always add PS Header if Space is present and visible. - position += mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems); + position = mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems); int privateSpaceState = mPrivateProviderManager.getCurrentState(); switch (privateSpaceState) { case PrivateProfileManager.STATE_DISABLED: @@ -321,43 +324,51 @@ public class AlphabeticalAppsList implement break; case PrivateProfileManager.STATE_ENABLED: // Add PS Apps only in Enabled State. - addAppsWithSections(mPrivateApps, position); - if (mActivityContext.getAppsView() != null) { - mActivityContext.getAppsView().getActiveRecyclerView() - .scrollToBottomWithMotion(); - } + position = addPrivateSpaceApps(position); break; } } + return position; } - private void addAppsWithSections(List appList, int startPosition) { + private int addPrivateSpaceApps(int position) { + // Add Install Apps Button first. + if (Flags.privateSpaceAppInstallerButton()) { + mPrivateProviderManager.addPrivateSpaceInstallAppButton(mAdapterItems); + position++; + } + + // Split of private space apps into user-installed and system apps. + Map> split = mPrivateApps.stream() + .collect(Collectors.partitioningBy(mPrivateProviderManager + .splitIntoUserInstalledAndSystemApps())); + // Add user installed apps + position = addAppsWithSections(split.get(true), position); + // Add system apps separator. + if (Flags.privateSpaceSysAppsSeparation()) { + position = mPrivateProviderManager.addSystemAppsDivider(mAdapterItems); + } + // Add system apps. + position = addAppsWithSections(split.get(false), position); + + return position; + } + + private int addAppsWithSections(List appList, int startPosition) { String lastSectionName = null; boolean hasPrivateApps = false; if (mPrivateProviderManager != null) { hasPrivateApps = appList.stream(). allMatch(mPrivateProviderManager.getItemInfoMatcher()); } - int privateAppCount = 0; - int numberOfColumns = mActivityContext.getDeviceProfile().numShownAllAppsColumns; - int numberOfAppRows = (int) Math.ceil((double) appList.size() / numberOfColumns); - for (AppInfo info : appList) { + for (int i = 0; i < appList.size(); i++) { + AppInfo info = appList.get(i); // Apply decorator to private apps. if (hasPrivateApps) { - int roundRegion = ROUND_NOTHING; - if ((privateAppCount / numberOfColumns) == numberOfAppRows - 1) { - if ((privateAppCount % numberOfColumns) == 0) { - // App is the first column - roundRegion = ROUND_BOTTOM_LEFT; - } else if ((privateAppCount % numberOfColumns) == numberOfColumns-1) { - roundRegion = ROUND_BOTTOM_RIGHT; - } - } mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info, new SectionDecorationInfo(mActivityContext.getApplicationContext(), - roundRegion, + getRoundRegions(i, appList.size()), true /* decorateTogether */))); - privateAppCount += 1; } else { mAdapterItems.add(AdapterItem.asApp(info)); } @@ -370,6 +381,44 @@ public class AlphabeticalAppsList implement } startPosition++; } + return startPosition; + } + + /** + * Determines the corner regions that should be rounded for a specific app icon based on its + * position in a grid. Apps that should only be cared about rounding are the apps in the last + * row. In the last row on the first column, the app should only be rounded on the bottom left. + * Apps in the middle would not be rounded and the last app on the last row will ALWAYS have a + * {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}. + * + * @param appIndex The index of the app icon within the app list. + * @param appListSize The total number of apps within the app list. + * @return An integer representing the corner regions to be rounded, using bitwise flags: + * - {@link SectionDecorationInfo#ROUND_NOTHING}: No corners should be rounded. + * - {@link SectionDecorationInfo#ROUND_TOP_LEFT}: Round the top-left corner. + * - {@link SectionDecorationInfo#ROUND_TOP_RIGHT}: Round the top-right corner. + * - {@link SectionDecorationInfo#ROUND_BOTTOM_LEFT}: Round the bottom-left corner. + * - {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}: Round the bottom-right corner. + */ + @VisibleForTesting + int getRoundRegions(int appIndex, int appListSize) { + int numberOfAppRows = (int) Math.ceil((double) appListSize / mNumAppsPerRowAllApps); + int roundRegion = ROUND_NOTHING; + // App is in the last row. + if ((appIndex / mNumAppsPerRowAllApps) == numberOfAppRows - 1) { + if ((appIndex % mNumAppsPerRowAllApps) == 0) { + // App is the first column. + roundRegion = ROUND_BOTTOM_LEFT; + } else if ((appIndex % mNumAppsPerRowAllApps) == mNumAppsPerRowAllApps-1) { + // App is in the last column. + roundRegion = ROUND_BOTTOM_RIGHT; + } + // Ensure the last private app is rounded on the bottom right. + if (appIndex == appListSize - 1) { + roundRegion |= ROUND_BOTTOM_RIGHT; + } + } + return roundRegion; } private static class MyDiffCallback extends DiffUtil.Callback { diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java index 311a40ef64..a0867dbaf1 100644 --- a/src/com/android/launcher3/allapps/AppInfoComparator.java +++ b/src/com/android/launcher3/allapps/AppInfoComparator.java @@ -43,9 +43,7 @@ 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 == null ? "" : a.title.toString(), - b.title == null ? "" : b.title.toString()); + int result = mLabelComparator.compare(getSortingTitle(a), getSortingTitle(b)); if (result != 0) { return result; } @@ -64,4 +62,14 @@ public class AppInfoComparator implements Comparator { return aUserSerial.compareTo(bUserSerial); } } + + private String getSortingTitle(AppInfo info) { + if (info.appTitle != null) { + return info.appTitle.toString(); + } + if (info.title != null) { + return info.title.toString(); + } + return ""; + } } diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 5eeb259fe5..5e5795d4e1 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -17,6 +17,7 @@ package com.android.launcher3.allapps; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT; +import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_LEFT; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_RIGHT; import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; @@ -35,6 +36,8 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Flags; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.allapps.search.SearchAdapterProvider; import com.android.launcher3.config.FeatureFlags; @@ -61,7 +64,8 @@ public abstract class BaseAllAppsAdapter ex public static final int VIEW_TYPE_WORK_EDU_CARD = 1 << 4; public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5; public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6; - public static final int NEXT_ID = 7; + public static final int VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER = 1 << 7; + public static final int NEXT_ID = 8; // Common view type masks public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; @@ -69,6 +73,8 @@ public abstract class BaseAllAppsAdapter ex public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER = VIEW_TYPE_PRIVATE_SPACE_HEADER; + public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_SYS_APPS_DIVIDER = + VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER; protected final SearchAdapterProvider mAdapterProvider; @@ -199,6 +205,11 @@ public abstract class BaseAllAppsAdapter ex return isViewType(viewType, VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER); } + /** Checks if the passed viewType represents private space system apps divider. */ + public static boolean isPrivateSpaceSysAppsDividerView(int viewType) { + return isViewType(viewType, VIEW_TYPE_MASK_PRIVATE_SPACE_SYS_APPS_DIVIDER); + } + public void setIconFocusListener(OnFocusChangeListener focusListener) { mIconFocusListener = focusListener; } @@ -212,8 +223,10 @@ public abstract class BaseAllAppsAdapter ex public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_ICON: - int layout = !FeatureFlags.enableTwolineAllapps() ? R.layout.all_apps_icon - : R.layout.all_apps_icon_twoline; + int layout = (Flags.enableTwolineToggle() + && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get( + mActivityContext.getApplicationContext())) + ? R.layout.all_apps_icon_twoline : R.layout.all_apps_icon; BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( layout, parent, false); icon.setLongPressTimeoutFactor(1f); @@ -227,9 +240,9 @@ public abstract class BaseAllAppsAdapter ex case VIEW_TYPE_EMPTY_SEARCH: return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, false)); - case VIEW_TYPE_ALL_APPS_DIVIDER: + case VIEW_TYPE_ALL_APPS_DIVIDER, VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER: return new ViewHolder(mLayoutInflater.inflate( - R.layout.all_apps_divider, parent, false)); + R.layout.private_space_divider, parent, false)); case VIEW_TYPE_WORK_EDU_CARD: return new ViewHolder(mLayoutInflater.inflate( R.layout.work_apps_edu, parent, false)); @@ -249,6 +262,7 @@ public abstract class BaseAllAppsAdapter ex @Override public void onBindViewHolder(ViewHolder holder, int position) { + holder.itemView.setVisibility(View.VISIBLE); switch (holder.getItemViewType()) { case VIEW_TYPE_ICON: { AdapterItem adapterItem = mApps.getAdapterItems().get(position); @@ -282,6 +296,11 @@ public abstract class BaseAllAppsAdapter ex new SectionDecorationInfo(mActivityContext, roundRegions, false /* decorateTogether */); break; + case VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER: + adapterItem = mApps.getAdapterItems().get(position); + adapterItem.decorationInfo = new SectionDecorationInfo(mActivityContext, + ROUND_NOTHING, true /* decorateTogether */); + break; case VIEW_TYPE_ALL_APPS_DIVIDER: case VIEW_TYPE_WORK_DISABLED_CARD: // nothing to do diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java index 1ba5f8e8cf..a1f6ebe557 100644 --- a/src/com/android/launcher3/allapps/FloatingHeaderView.java +++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java @@ -225,10 +225,10 @@ public class FloatingHeaderView extends LinearLayout implements for (FloatingHeaderRow row : mAllRows) { row.setup(this, mAllRows, tabsHidden); } - updateExpectedHeight(); mTabsHidden = tabsHidden; maybeSetTabVisibility(VISIBLE); + updateExpectedHeight(); mMainRV = mainRV; mWorkRV = workRV; mSearchRV = searchRV; diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java index 5e48177d75..63a168e4e3 100644 --- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java @@ -21,7 +21,6 @@ import android.view.WindowInsets; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.Utilities; import com.android.launcher3.statemanager.StateManager; /** @@ -43,11 +42,7 @@ public class LauncherAllAppsContainerView extends ActivityAllAppsContainerView mAllApps; private final Predicate mPrivateProfileMatcher; + private Set mPreInstalledSystemPackages = new HashSet<>(); + private Intent mAppInstallerIntent = new Intent(); private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator; private boolean mPrivateSpaceSettingsAvailable; + private Runnable mUnlockRunnable; public PrivateProfileManager(UserManager userManager, ActivityAllAppsContainerView allApps, @@ -61,7 +79,7 @@ public class PrivateProfileManager extends UserProfileManager { super(userManager, statsLogManager, userCache); mAllApps = allApps; mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate(); - UI_HELPER_EXECUTOR.post(this::setPrivateSpaceSettingsAvailable); + UI_HELPER_EXECUTOR.post(this::initializeInBackgroundThread); } /** Adds Private Space Header to the layout. */ @@ -71,9 +89,50 @@ public class PrivateProfileManager extends UserProfileManager { return adapterItems.size(); } - /** Disables quiet mode for Private Space User Profile. */ - public void unlockPrivateProfile() { + /** Adds Private Space System Apps Divider to the layout. */ + public int addSystemAppsDivider(List adapterItems) { + adapterItems.add(new BaseAllAppsAdapter + .AdapterItem(VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER)); + mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); + return adapterItems.size(); + } + + /** Adds Private Space install app button to the layout. */ + public void addPrivateSpaceInstallAppButton(List adapterItems) { + Context context = mAllApps.getContext(); + // Prepare bitmapInfo + Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext( + context, com.android.launcher3.R.drawable.private_space_install_app_icon); + BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut); + + AppInfo itemInfo = new AppInfo(); + itemInfo.title = context.getResources().getString(R.string.ps_add_button_label); + itemInfo.intent = mAppInstallerIntent; + itemInfo.bitmap = bitmapInfo; + itemInfo.contentDescription = context.getResources().getString( + com.android.launcher3.R.string.ps_add_button_content_description); + itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP | FLAG_NOT_PINNABLE; + + BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON); + item.itemInfo = itemInfo; + item.decorationInfo = new SectionDecorationInfo(context, ROUND_NOTHING, + /* decorateTogether */ true); + + adapterItems.add(item); + mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); + } + + /** + * Disables quiet mode for Private Space User Profile. + * The runnable passed will be executed in the {@link #reset()} method, + * when Launcher receives update about profile availability. + * The runnable passed is only executed once, and reset after execution. + * In case the method is called again, before the previously set runnable was executed, + * the runnable will be updated. + */ + public void unlockPrivateProfile(Runnable runnable) { enableQuietMode(false); + mUnlockRunnable = runnable; } /** Enables quiet mode for Private Space User Profile. */ @@ -89,35 +148,71 @@ public class PrivateProfileManager extends UserProfileManager { /** Resets the current state of Private Profile, w.r.t. to Launcher. */ public void reset() { + int previousState = getCurrentState(); boolean isEnabled = !mAllApps.getAppsStore() .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED); int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED; setCurrentState(updatedState); resetPrivateSpaceDecorator(updatedState); + if (transitioningFromLockedToUnlocked(previousState, updatedState)) { + applyUnlockRunnable(); + } } - /** Opens the Private Space Settings Entry Point. */ + /** Opens the Private Space Settings Page. */ public void openPrivateSpaceSettings() { - Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT); - psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE); - mAllApps.getContext().startActivity(psSettingsIntent); + if (mPrivateSpaceSettingsAvailable) { + mAllApps.getContext().startActivity(PRIVATE_SPACE_INTENT); + } } - /** Whether Private Space Settings Entry Point is available on the device. */ + /** Returns whether or not Private Space Settings Page is available. */ public boolean isPrivateSpaceSettingsAvailable() { return mPrivateSpaceSettingsAvailable; } - private void setPrivateSpaceSettingsAvailable() { - if (mPrivateSpaceSettingsAvailable) { - return; - } + /** Sets whether Private Space Settings Page is available. */ + public boolean setPrivateSpaceSettingsAvailable(boolean value) { + return mPrivateSpaceSettingsAvailable = value; + } + + /** Initializes binder call based properties in non-main thread. + *

+ * This can cause the Private Space container items to not load/respond correctly sometimes, + * when the All Apps Container loads for the first time (device restarts, new profiles + * added/removed, etc.), as the properties are being set in non-ui thread whereas the container + * loads in the ui thread. + * This case should still be ok, as locking the Private Space container and unlocking it, + * reloads the values, fixing the incorrect UI. + */ + private void initializeInBackgroundThread() { + Preconditions.assertNonUiThread(); + setPreInstalledSystemPackages(); + setAppInstallerIntent(); + initializePrivateSpaceSettingsState(); + } + + private void initializePrivateSpaceSettingsState() { Preconditions.assertNonUiThread(); - Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT); - psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE); ResolveInfo resolveInfo = mAllApps.getContext().getPackageManager() - .resolveActivity(psSettingsIntent, PackageManager.MATCH_SYSTEM_ONLY); - mPrivateSpaceSettingsAvailable = resolveInfo != null; + .resolveActivity(PRIVATE_SPACE_INTENT, PackageManager.MATCH_SYSTEM_ONLY); + setPrivateSpaceSettingsAvailable(resolveInfo != null); + } + + private void setPreInstalledSystemPackages() { + Preconditions.assertNonUiThread(); + if (getProfileUser() != null) { + mPreInstalledSystemPackages = new HashSet<>(ApiWrapper + .getPreInstalledSystemPackages(mAllApps.getContext(), getProfileUser())); + } + } + + private void setAppInstallerIntent() { + Preconditions.assertNonUiThread(); + if (getProfileUser() != null) { + mAppInstallerIntent = ApiWrapper.getAppMarketActivityIntent(mAllApps.getContext(), + BuildConfig.APPLICATION_ID, getProfileUser()); + } } @VisibleForTesting @@ -138,13 +233,6 @@ public class PrivateProfileManager extends UserProfileManager { } // Add Private Space Decorator to the Recycler view. mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator); - if (Flags.privateSpaceAnimation() && mAllApps.getActiveRecyclerView() - == mainAdapterHolder.mRecyclerView) { - RecyclerViewAnimationController recyclerViewAnimationController = - new RecyclerViewAnimationController(mAllApps); - recyclerViewAnimationController.animateToState(true /* expand */, - ANIMATION_DURATION, () -> {}); - } } else { // Remove Private Space Decorator from the Recycler view. if (mPrivateAppsSectionDecorator != null) { @@ -158,8 +246,30 @@ public class PrivateProfileManager extends UserProfileManager { setQuietMode(enable); } + void applyUnlockRunnable() { + if (mUnlockRunnable != null) { + // reset the runnable to prevent re-execution. + MAIN_EXECUTOR.post(mUnlockRunnable); + mUnlockRunnable = null; + } + } + + private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) { + return previousState == STATE_DISABLED && updatedState == STATE_ENABLED; + } + @Override public Predicate getUserMatcher() { return mPrivateProfileMatcher; } + + /** + * Splits private apps into user installed and system apps. + * When the list of system apps is empty, all apps are treated as system. + */ + public Predicate splitIntoUserInstalledAndSystemApps() { + return appInfo -> !mPreInstalledSystemPackages.isEmpty() + && (appInfo.componentName == null + || !(mPreInstalledSystemPackages.contains(appInfo.componentName.getPackageName()))); + } } diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java index 568ce32fb9..6067454812 100644 --- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java +++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java @@ -16,6 +16,13 @@ package com.android.launcher3.allapps; +import static android.view.View.GONE; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; + +import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION; @@ -23,31 +30,63 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP; -import android.view.View; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.LayoutTransition; +import android.animation.ValueAnimator; +import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.app.animation.Interpolators; +import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.allapps.UserProfileManager.UserProfileState; +import com.android.launcher3.anim.AnimatedPropertySetter; +import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.views.RecyclerViewFastScroller; + +import java.util.List; /** * Controller which returns views to be added to Private Space Header based upon * {@link UserProfileState} */ public class PrivateSpaceHeaderViewController { + private static final int EXPAND_SCROLL_DURATION = 2000; + private static final int EXPAND_COLLAPSE_DURATION = 800; + private static final int SETTINGS_OPACITY_DURATION = 160; + private final ActivityAllAppsContainerView mAllApps; private final PrivateProfileManager mPrivateProfileManager; - public PrivateSpaceHeaderViewController(PrivateProfileManager privateProfileManager) { + public PrivateSpaceHeaderViewController(ActivityAllAppsContainerView allApps, + PrivateProfileManager privateProfileManager) { + this.mAllApps = allApps; this.mPrivateProfileManager = privateProfileManager; } /** Add Private Space Header view elements based upon {@link UserProfileState} */ public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { + // Set the transition duration for the settings and lock button to animate. + ViewGroup settingsAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); + LayoutTransition settingsAndLockTransition = settingsAndLockGroup.getLayoutTransition(); + settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); + settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); + //Add quietMode image and action for lock/unlock button - ImageButton quietModeButton = parent.findViewById(R.id.ps_lock_unlock_button); - assert quietModeButton != null; - addQuietModeButton(quietModeButton); + ViewGroup lockButton = + parent.findViewById(R.id.ps_lock_unlock_button); + assert lockButton != null; + addLockButton(parent, lockButton); + + //Trigger lock/unlock action from header. + addHeaderOnClickListener(parent); //Add image and action for private space settings button ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); @@ -60,53 +99,189 @@ public class PrivateSpaceHeaderViewController { addTransitionImage(transitionView); } - private void addQuietModeButton(ImageButton quietModeButton) { + /** + * Adds the quietModeButton and attach onClickListener for the header to animate different + * states when clicked. + */ + private void addLockButton(ViewGroup psHeader, ViewGroup lockButton) { + TextView lockText = lockButton.findViewById(R.id.lock_text); switch (mPrivateProfileManager.getCurrentState()) { case STATE_ENABLED -> { - quietModeButton.setVisibility(View.VISIBLE); - quietModeButton.setImageResource(R.drawable.bg_ps_lock_button); - quietModeButton.setOnClickListener( - view -> { - mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP); - mPrivateProfileManager.lockPrivateProfile(); - }); + lockText.setVisibility(VISIBLE); + lockButton.setVisibility(VISIBLE); + lockButton.setOnClickListener(view -> lockAction(psHeader)); } case STATE_DISABLED -> { - quietModeButton.setVisibility(View.VISIBLE); - quietModeButton.setImageResource(R.drawable.bg_ps_unlock_button); - quietModeButton.setOnClickListener( - view -> { - mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); - mPrivateProfileManager.unlockPrivateProfile(); - }); + lockText.setVisibility(GONE); + lockButton.setVisibility(VISIBLE); + lockButton.setOnClickListener(view -> unlockAction(psHeader)); } - default -> quietModeButton.setVisibility(View.GONE); + default -> lockButton.setVisibility(GONE); + } + } + + private void addHeaderOnClickListener(RelativeLayout header) { + if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) { + header.setOnClickListener(view -> unlockAction(header)); + } else { + header.setOnClickListener(null); + } + } + + private void unlockAction(ViewGroup psHeader) { + mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); + mPrivateProfileManager.unlockPrivateProfile((() -> onPrivateProfileUnlocked(psHeader))); + } + + private void lockAction(ViewGroup psHeader) { + mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP); + if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()) { + updatePrivateStateAnimator(false, psHeader); + } else { + mPrivateProfileManager.lockPrivateProfile(); } } private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) { - settingsButton.setVisibility(View.VISIBLE); + settingsButton.setVisibility(VISIBLE); + settingsButton.setAlpha(1f); settingsButton.setOnClickListener( view -> { mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP); mPrivateProfileManager.openPrivateSpaceSettings(); }); } else { - settingsButton.setVisibility(View.GONE); + settingsButton.setVisibility(GONE); } } private void addTransitionImage(ImageView transitionImage) { if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) { - transitionImage.setVisibility(View.VISIBLE); + transitionImage.setVisibility(VISIBLE); } else { - transitionImage.setVisibility(View.GONE); + transitionImage.setVisibility(GONE); + } + } + + private void onPrivateProfileUnlocked(ViewGroup header) { + // If we are on main adapter view, we apply the PS Container expansion animation and + // then scroll down to load the entire container, making animation visible. + ActivityAllAppsContainerView.AdapterHolder mainAdapterHolder = + (ActivityAllAppsContainerView.AdapterHolder) mAllApps.mAH.get(MAIN); + if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation() + && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) { + // Animate the text and settings icon. + updatePrivateStateAnimator(true, header); + mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION); + } + } + + /** Finds the private space header to scroll to and set the private space icons to GONE. */ + private void collapse() { + AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); + for (int i = allAppsRecyclerView.getChildCount() - 1; i > 0; i--) { + int adapterPosition = allAppsRecyclerView.getChildAdapterPosition( + allAppsRecyclerView.getChildAt(i)); + List allAppsAdapters = allAppsRecyclerView.getApps() + .getAdapterItems(); + if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) { + continue; + } + // Scroll to the private space header. + if (allAppsAdapters.get(adapterPosition).viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { + // Note: SmoothScroller is meant to be used once. + RecyclerView.SmoothScroller smoothScroller = + new LinearSmoothScroller(mAllApps.getContext()) { + @Override protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_END; + } + }; + smoothScroller.setTargetPosition(adapterPosition); + RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); + if (layoutManager != null) { + layoutManager.startSmoothScroll(smoothScroller); + } + break; + } + // Make the private space apps gone to "collapse". + if (allAppsAdapters.get(adapterPosition).decorationInfo != null) { + allAppsRecyclerView.getChildAt(i).setVisibility(GONE); + } } } PrivateProfileManager getPrivateProfileManager() { return mPrivateProfileManager; } + + /** + * Scrolls up to the private space header and animates the collapsing of the text. + */ + private ValueAnimator animateCollapseAnimation(ViewGroup lockButton) { + float from = 1; + float to = 0; + RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar(); + ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to); + collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION); + collapseAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (scrollBar != null) { + scrollBar.setVisibility(INVISIBLE); + } + // scroll up + collapse(); + // Animate the collapsing of the text. + lockButton.findViewById(R.id.lock_text).setVisibility(GONE); + } + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (scrollBar != null) { + scrollBar.setThumbOffsetY(-1); + scrollBar.setVisibility(VISIBLE); + } + mPrivateProfileManager.lockPrivateProfile(); + } + }); + return collapseAnim; + } + + /** + * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an + * animation. At the moment, collapsing, setting alpha changes, and animating the text is done + * here. + */ + private void updatePrivateStateAnimator(boolean expand, ViewGroup psHeader) { + PropertySetter setter = new AnimatedPropertySetter(); + ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); + ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); + updateSettingsGearAlpha(settingsButton, expand, setter); + AnimatorSet animatorSet = setter.buildAnim(); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Animate the collapsing of the text at the same time while updating lock button. + lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); + } + }); + // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the + // header. Otherwise the smooth scrolling will scroll higher when played with the state + // animator. + if (!expand) { + animatorSet.playTogether(animateCollapseAnimation(lockButton)); + } + animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); + animatorSet.start(); + } + + /** Change the settings gear alpha when expanded or collapsed. */ + private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand, + PropertySetter setter) { + float toAlpha = expand ? 1 : 0; + setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR) + .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0); + } } diff --git a/src/com/android/launcher3/allapps/SectionDecorationInfo.java b/src/com/android/launcher3/allapps/SectionDecorationInfo.java index 1fed2b654e..c438d19b3e 100644 --- a/src/com/android/launcher3/allapps/SectionDecorationInfo.java +++ b/src/com/android/launcher3/allapps/SectionDecorationInfo.java @@ -22,11 +22,11 @@ import androidx.annotation.NonNull; public class SectionDecorationInfo { - public static final int ROUND_NOTHING = 1 << 1; - public static final int ROUND_TOP_LEFT = 1 << 2; - public static final int ROUND_TOP_RIGHT = 1 << 3; - public static final int ROUND_BOTTOM_LEFT = 1 << 4; - public static final int ROUND_BOTTOM_RIGHT = 1 << 5; + public static final int ROUND_NOTHING = 0; + public static final int ROUND_TOP_LEFT = 1 << 1; + public static final int ROUND_TOP_RIGHT = 1 << 2; + public static final int ROUND_BOTTOM_LEFT = 1 << 3; + public static final int ROUND_BOTTOM_RIGHT = 1 << 4; public static final int DECORATOR_ALPHA = 255; protected boolean mShouldDecorateItemsTogether; diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java index 0261010d8b..6a1f37a28d 100644 --- a/src/com/android/launcher3/allapps/UserProfileManager.java +++ b/src/com/android/launcher3/allapps/UserProfileManager.java @@ -22,9 +22,7 @@ import android.os.UserHandle; import android.os.UserManager; import androidx.annotation.IntDef; -import androidx.annotation.VisibleForTesting; -import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.UserCache; @@ -71,16 +69,13 @@ public abstract class UserProfileManager { /** Sets quiet mode as enabled/disabled for the profile type. */ protected void setQuietMode(boolean enabled) { - if (Utilities.ATLEAST_P) { - UI_HELPER_EXECUTOR.post(() -> { + UI_HELPER_EXECUTOR.post(() -> mUserCache.getUserProfiles() .stream() .filter(getUserMatcher()) .findFirst() .ifPresent(userHandle -> - mUserManager.requestQuietModeEnabled(enabled, userHandle)); - }); - } + mUserManager.requestQuietModeEnabled(enabled, userHandle))); } /** Sets current state for the profile type. */ @@ -89,11 +84,23 @@ public abstract class UserProfileManager { } /** Returns current state for the profile type. */ - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public int getCurrentState() { return mCurrentState; } + /** Returns if user profile is enabled. */ + public boolean isEnabled() { + return mCurrentState == STATE_ENABLED; + } + + /** Returns the UserHandle corresponding to the profile type, null in case no matches found. */ + public UserHandle getProfileUser() { + return mUserCache.getUserProfiles().stream() + .filter(getUserMatcher()) + .findAny() + .orElse(null); + } + /** Logs Event to StatsLogManager. */ protected void logEvents(StatsLogManager.EventEnum event) { mStatsLogManager.logger().log(event); diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java index 48400b23b9..eb7d429f56 100644 --- a/src/com/android/launcher3/allapps/WorkModeSwitch.java +++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java @@ -83,11 +83,9 @@ public class WorkModeSwitch extends LinearLayout implements Insettable, mIcon = findViewById(R.id.work_icon); mTextView = findViewById(R.id.pause_text); setSelected(true); - if (Utilities.ATLEAST_R) { - KeyboardInsetAnimationCallback keyboardInsetAnimationCallback = - new KeyboardInsetAnimationCallback(this); - setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback); - } + KeyboardInsetAnimationCallback keyboardInsetAnimationCallback = + new KeyboardInsetAnimationCallback(this); + setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback); setInsets(mActivityContext.getDeviceProfile().getInsets()); updateStringFromCache(); diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java index 18826673a1..e1eeabe258 100644 --- a/src/com/android/launcher3/allapps/WorkPausedCard.java +++ b/src/com/android/launcher3/allapps/WorkPausedCard.java @@ -26,7 +26,6 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.model.StringCache; import com.android.launcher3.views.ActivityContext; @@ -80,11 +79,9 @@ public class WorkPausedCard extends LinearLayout implements View.OnClickListener @Override public void onClick(View view) { - if (Utilities.ATLEAST_P) { - setEnabled(false); - mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true); - mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP); - } + setEnabled(false); + mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true); + mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP); } @Override diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java index c430a36279..a54e52c905 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -199,8 +199,7 @@ public class WorkProfileManager extends UserProfileManager } private void onWorkFabClicked(View view) { - if (Utilities.ATLEAST_P && getCurrentState() == STATE_ENABLED - && mWorkModeSwitch.isEnabled()) { + if (getCurrentState() == STATE_ENABLED && mWorkModeSwitch.isEnabled()) { logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP); setWorkProfileEnabled(false); } diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java index 4427a49dab..f9d047b980 100644 --- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java +++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java @@ -20,6 +20,7 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.SuggestionSpan; +import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnFocusChangeListener; @@ -42,6 +43,7 @@ public class AllAppsSearchBarController implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener, OnFocusChangeListener { + private static final String TAG = "AllAppsSearchBarController"; protected ActivityContext mLauncher; protected SearchCallback mCallback; protected ExtendedEditText mInput; @@ -122,6 +124,7 @@ public class AllAppsSearchBarController public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) { + Log.i(TAG, "User tapped ime search button"); // selectFocusedView should return SearchTargetEvent that is passed onto onClick return mLauncher.getAppsView().getMainAdapterProvider().launchHighlightedItem(); } diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java index 2f3fa63321..b414ab6e35 100644 --- a/src/com/android/launcher3/anim/AnimatedFloat.java +++ b/src/com/android/launcher3/anim/AnimatedFloat.java @@ -109,6 +109,13 @@ public class AnimatedFloat { public void cancelAnimation() { if (mValueAnimator != null) { mValueAnimator.cancel(); + // Clears the property values, so further ObjectAnimator#setCurrentFraction from e.g. + // AnimatorPlaybackController calls would do nothing. The null check is necessary to + // avoid mValueAnimator being set to null in onAnimationEnd. + if (mValueAnimator != null) { + mValueAnimator.setValues(); + mValueAnimator = null; + } } } diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java index fd731f473e..e58890f7ca 100644 --- a/src/com/android/launcher3/anim/PendingAnimation.java +++ b/src/com/android/launcher3/anim/PendingAnimation.java @@ -25,7 +25,6 @@ import android.animation.ValueAnimator; import android.os.Trace; import android.util.FloatProperty; -import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController.Holder; import java.util.ArrayList; @@ -84,9 +83,23 @@ public class PendingAnimation extends AnimatedPropertySetter { add(anim); } + /** + * Add an {@link AnimatedFloat} to the animation. + *

+ * Different from {@link #addFloat}, this method use animator provided by + * {@link AnimatedFloat#animateToValue}, which tracks the animator inside the AnimatedFloat, + * allowing the animation to be canceled and animate again from AnimatedFloat side. + */ + public void addAnimatedFloat(AnimatedFloat target, float from, float to, + TimeInterpolator interpolator) { + Animator anim = target.animateToValue(from, to); + anim.setInterpolator(interpolator); + add(anim); + } + /** If trace is enabled, add counter to trace animation progress. */ public void logAnimationProgressToTrace(String counterName) { - if (Utilities.ATLEAST_Q && Trace.isEnabled()) { + if (Trace.isEnabled()) { super.addOnFrameListener( animation -> Trace.setCounter( counterName, (long) (animation.getAnimatedFraction() * 100))); diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index 1d73441b71..48d0fbd485 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -17,8 +17,10 @@ package com.android.launcher3.apppairs; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,11 +34,13 @@ import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; import java.util.Collections; import java.util.Comparator; +import java.util.function.Predicate; /** * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace. @@ -45,6 +49,13 @@ import java.util.Comparator; * member apps are set into these rectangles. */ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable { + private static final String TAG = "AppPairIcon"; + + /** + * Indicates that the app pair is currently launchable on the current screen. + */ + private boolean mIsLaunchableAtScreenSize = true; + // A view that holds the app pair icon graphic. private AppPairIconGraphic mIconGraphic; // A view that holds the app pair's title. @@ -83,6 +94,13 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab icon.setOnClickListener(activity.getItemOnClickListener()); icon.mInfo = appPairInfo; + if (icon.mInfo.contents.size() != 2) { + Log.wtf(TAG, "AppPair contents not 2, size: " + icon.mInfo.contents.size()); + return icon; + } + + icon.checkScreenSize(); + // Set up icon drawable area icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic); icon.mIconGraphic.init(activity.getDeviceProfile(), icon); @@ -96,8 +114,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab icon.mAppPairName.setText(appPairInfo.title); // Set up accessibility - icon.setContentDescription(icon.getAccessibilityTitle( - appPairInfo.contents.get(0).title, appPairInfo.contents.get(1).title)); + icon.setContentDescription(icon.getAccessibilityTitle(appPairInfo)); icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); return icon; @@ -106,7 +123,9 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab /** * Returns a formatted accessibility title for app pairs. */ - public String getAccessibilityTitle(CharSequence app1, CharSequence app2) { + public String getAccessibilityTitle(FolderInfo appPairInfo) { + CharSequence app1 = appPairInfo.contents.get(0).title; + CharSequence app2 = appPairInfo.contents.get(1).title; return getContext().getString(R.string.app_pair_name_format, app1, app2); } @@ -158,4 +177,40 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab public View getIconDrawableArea() { return mIconGraphic; } + + public boolean isLaunchableAtScreenSize() { + return mIsLaunchableAtScreenSize; + } + + /** + * Checks if the app pair is launchable in the current device configuration. + * + * App pairs can be "disabled" in two ways: + * 1) One of the member WorkspaceItemInfos is disabled (i.e. the app software itself is paused + * by the user or can't be launched). + * 2) This specific instance of an app pair can't be launched due to screen size requirements. + * + * This method checks and updates #2. Both #1 and #2 are checked when app pairs are drawn + * {@link AppPairIconGraphic#dispatchDraw(Canvas)} or clicked on + * {@link com.android.launcher3.touch.ItemClickHandler#onClickAppPairIcon(View)} + */ + public void checkScreenSize() { + DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile(); + // If user is on a small screen, we can't launch if either of the apps is non-resizeable + mIsLaunchableAtScreenSize = + dp.isTablet || getInfo().contents.stream().noneMatch( + wii -> wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE)); + } + + /** + * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn. + */ + public void maybeRedrawForWorkspaceUpdate(Predicate itemCheck) { + // If either of the app pair icons return true on the predicate (i.e. in the list of + // updated apps), redraw the icon graphic (icon background and both icons). + if (getInfo().contents.stream().anyMatch(itemCheck)) { + checkScreenSize(); + mIconGraphic.invalidate(); + } + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconBackground.java b/src/com/android/launcher3/apppairs/AppPairIconBackground.java index 4e60ece170..b5011f126c 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconBackground.java +++ b/src/com/android/launcher3/apppairs/AppPairIconBackground.java @@ -157,7 +157,7 @@ class AppPairIconBackground extends Drawable { @Override public void setAlpha(int i) { - // Required by Drawable but not used. + mBackgroundPaint.setAlpha(i); } @Override diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index 29459796bc..365edf8125 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -21,9 +21,14 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log import android.view.Gravity import android.widget.FrameLayout import com.android.launcher3.DeviceProfile +import com.android.launcher3.icons.BitmapInfo +import com.android.launcher3.icons.PlaceHolderIconDrawable +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.Themes /** * A FrameLayout marking the area on an [AppPairIcon] where the visual icon will be drawn. One of @@ -31,6 +36,8 @@ import com.android.launcher3.DeviceProfile */ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + private val TAG = "AppPairIconGraphic" + companion object { // Design specs -- the below ratios are in relation to the size of a standard app icon. private const val OUTER_PADDING_SCALE = 1 / 30f @@ -39,6 +46,9 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr private const val CENTER_CHANNEL_SCALE = 1 / 30f private const val BIG_RADIUS_SCALE = 1 / 5f private const val SMALL_RADIUS_SCALE = 1 / 15f + // Disabled alpha is 38%, or 97/255 + private const val DISABLED_ALPHA = 97 + private const val ENABLED_ALPHA = 255 } // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on @@ -61,8 +71,8 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr private lateinit var parentIcon: AppPairIcon private lateinit var appPairBackground: Drawable - private lateinit var appIcon1: Drawable - private lateinit var appIcon2: Drawable + private var appIcon1: Drawable? = null + private var appIcon2: Drawable? = null fun init(grid: DeviceProfile, icon: AppPairIcon) { // Calculate device-specific measurements @@ -79,10 +89,38 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr appPairBackground = AppPairIconBackground(context, this) appPairBackground.setBounds(0, 0, backgroundSize.toInt(), backgroundSize.toInt()) - appIcon1 = parentIcon.info.contents[0].newIcon(context) - appIcon2 = parentIcon.info.contents[1].newIcon(context) - appIcon1.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) - appIcon2.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) + applyIcons(parentIcon.info.contents) + + // Center the drawable area in the larger icon canvas + val lp: LayoutParams = layoutParams as LayoutParams + lp.gravity = Gravity.CENTER_HORIZONTAL + lp.topMargin = outerPadding.toInt() + lp.height = backgroundSize.toInt() + lp.width = backgroundSize.toInt() + layoutParams = lp + } + + /** Sets up app pair member icons for drawing. */ + private fun applyIcons(contents: ArrayList) { + // App pair should always contain 2 members; if not 2, return to avoid a crash loop + if (contents.size != 2) { + Log.wtf(TAG, "AppPair contents not 2, size: " + contents.size, Throwable()) + return + } + + // Generate new icons, using themed flag if needed + val flags = if (Themes.isThemedIconEnabled(context)) BitmapInfo.FLAG_THEMED else 0 + val newIcon1 = parentIcon.info.contents[0].newIcon(context, flags) + val newIcon2 = parentIcon.info.contents[1].newIcon(context, flags) + + // If app icons did not draw fully last time, animate to full icon + (appIcon1 as? PlaceHolderIconDrawable)?.animateIconUpdate(newIcon1) + (appIcon2 as? PlaceHolderIconDrawable)?.animateIconUpdate(newIcon2) + + appIcon1 = newIcon1 + appIcon2 = newIcon2 + appIcon1?.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) + appIcon2?.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt()) } /** Gets this icon graphic's bounds, with respect to the parent icon's coordinate system. */ @@ -99,17 +137,17 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) - // Center the drawable area in the larger icon canvas - val lp: LayoutParams = layoutParams as LayoutParams - lp.gravity = Gravity.CENTER_HORIZONTAL - lp.topMargin = outerPadding.toInt() - lp.height = backgroundSize.toInt() - lp.width = backgroundSize.toInt() - layoutParams = lp + val drawAlpha = + if (!parentIcon.isLaunchableAtScreenSize || parentIcon.info.isDisabled) DISABLED_ALPHA + else ENABLED_ALPHA // Draw background + appPairBackground.alpha = drawAlpha appPairBackground.draw(canvas) + // Make sure icons are loaded and fresh + applyIcons(parentIcon.info.contents) + // Draw first icon canvas.save() // The app icons are placed differently depending on device orientation. @@ -118,7 +156,8 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr } else { canvas.translate(width / 2f - memberIconSize / 2f, innerPadding) } - appIcon1.draw(canvas) + appIcon1?.alpha = drawAlpha + appIcon1?.draw(canvas) canvas.restore() // Draw second icon @@ -135,7 +174,8 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr height - (innerPadding + memberIconSize) ) } - appIcon2.draw(canvas) + appIcon2?.alpha = drawAlpha + appIcon2?.draw(canvas) canvas.restore() } } diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt index 16b185495a..e6654b1547 100644 --- a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt +++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt @@ -1,6 +1,7 @@ package com.android.launcher3.backuprestore import android.content.Context +import androidx.annotation.StringDef import com.android.launcher3.LauncherSettings.Favorites import com.android.launcher3.R import com.android.launcher3.util.ResourceBasedOverride @@ -11,6 +12,39 @@ import com.android.launcher3.util.ResourceBasedOverride */ open class LauncherRestoreEventLogger : ResourceBasedOverride { + /** Enumeration of potential errors returned to calls of pause/resume app updates. */ + @Retention(AnnotationRetention.SOURCE) + @StringDef( + RestoreError.PROFILE_DELETED, + RestoreError.MISSING_INFO, + RestoreError.MISSING_WIDGET_PROVIDER, + RestoreError.INVALID_LOCATION, + RestoreError.SHORTCUT_NOT_FOUND, + RestoreError.APP_NOT_INSTALLED, + RestoreError.WIDGETS_DISABLED, + RestoreError.PROFILE_NOT_RESTORED, + RestoreError.WIDGET_REMOVED, + RestoreError.GRID_MIGRATION_FAILURE, + RestoreError.NO_SEARCH_WIDGET, + RestoreError.INVALID_WIDGET_ID + ) + annotation class RestoreError { + companion object { + const val PROFILE_DELETED = "user_profile_deleted" + const val MISSING_INFO = "missing_information_when_loading" + const val MISSING_WIDGET_PROVIDER = "missing_widget_provider" + const val INVALID_LOCATION = "invalid_size_or_location" + const val SHORTCUT_NOT_FOUND = "shortcut_not_found" + const val APP_NOT_INSTALLED = "app_not_installed" + const val WIDGETS_DISABLED = "widgets_disabled" + const val PROFILE_NOT_RESTORED = "profile_not_restored" + const val WIDGET_REMOVED = "widget_not_found" + const val GRID_MIGRATION_FAILURE = "grid_migration_failed" + const val NO_SEARCH_WIDGET = "no_search_widget" + const val INVALID_WIDGET_ID = "invalid_widget_id" + } + } + companion object { const val TAG = "LauncherRestoreEventLogger" @@ -53,13 +87,23 @@ open class LauncherRestoreEventLogger : ResourceBasedOverride { // no-op } + /** + * Helper to log successfully restoring multiple items from the Favorites table. + * + * @param favoritesId The id of the item type from [Favorites] that was restored. + * @param count number of items that restored. + */ + open fun logFavoritesItemsRestored(favoritesId: Int, count: Int) { + // no-op + } + /** * Helper to log a failure to restore a single item from the Favorites table. * * @param favoritesId The id of the item type from [Favorites] that was not restored. * @param error error type for why the data was not restored. */ - open fun logSingleFavoritesItemRestoreFailed(favoritesId: Int, error: String?) { + open fun logSingleFavoritesItemRestoreFailed(favoritesId: Int, @RestoreError error: String?) { // no-op } @@ -70,7 +114,11 @@ open class LauncherRestoreEventLogger : ResourceBasedOverride { * @param count number of items that failed to restore. * @param error error type for why the data was not restored. */ - open fun logFavoritesItemsRestoreFailed(favoritesId: Int, count: Int, error: String?) { + open fun logFavoritesItemsRestoreFailed( + favoritesId: Int, + count: Int, + @RestoreError error: String? + ) { // no-op } diff --git a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java index 8754b748e0..c3037831a0 100644 --- a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java +++ b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java @@ -49,21 +49,37 @@ public class ReorderAlgorithm { * When changing the size of the widget this method will try first subtracting -1 in the x * dimension and then subtracting -1 in the y dimension until finding a possible solution or * until it no longer can reduce the span. - * * @param decX whether it will decrease the horizontal or vertical span if it can't find a * solution for the current span. * @return the same solution variable */ public ItemConfiguration findReorderSolution(ReorderParameters reorderParameters, boolean decX) { + return findReorderSolution(reorderParameters, mCellLayout.mDirectionVector, decX); + } + + /** + * This method differs from closestEmptySpaceReorder and dropInPlaceSolution because this method + * will move items around and will change the shape of the item if possible to try to find a + * solution. + *

+ * When changing the size of the widget this method will try first subtracting -1 in the x + * dimension and then subtracting -1 in the y dimension until finding a possible solution or + * until it no longer can reduce the span. + * @param direction Direction to attempt to push items if needed + * @param decX whether it will decrease the horizontal or vertical span if it can't find a + * solution for the current span. + * @return the same solution variable + */ + public ItemConfiguration findReorderSolution(ReorderParameters reorderParameters, + int[] direction, boolean decX) { return findReorderSolutionRecursive(reorderParameters.getPixelX(), reorderParameters.getPixelY(), reorderParameters.getMinSpanX(), reorderParameters.getMinSpanY(), reorderParameters.getSpanX(), - reorderParameters.getSpanY(), mCellLayout.mDirectionVector, + reorderParameters.getSpanY(), direction, reorderParameters.getDragView(), decX, reorderParameters.getSolution()); } - private ItemConfiguration findReorderSolutionRecursive(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { diff --git a/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt b/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt new file mode 100644 index 0000000000..62b19d4d04 --- /dev/null +++ b/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 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.celllayout + +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.areAnimatorsEnabled +import android.util.ArrayMap +import android.view.View +import com.android.app.animation.Interpolators.DECELERATE_1_5 +import com.android.launcher3.CellLayout +import com.android.launcher3.CellLayout.REORDER_ANIMATION_DURATION +import com.android.launcher3.Reorderable +import com.android.launcher3.Workspace +import com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET +import com.android.launcher3.util.Thunk +import kotlin.math.abs +import kotlin.math.atan +import kotlin.math.cos +import kotlin.math.sign +import kotlin.math.sin + +/** + * Class which represents the reorder preview animations. These animations show that an item is in a + * temporary state, and hint at where the item will return to. + */ +class ReorderPreviewAnimation( + val child: T, + // If the mode is MODE_HINT it will only move one period and stop, it then will be going + // backwards to the initial position, otherwise it will oscillate. + val mode: Int, + cellX0: Int, + cellY0: Int, + cellX1: Int, + cellY1: Int, + spanX: Int, + spanY: Int, + reorderMagnitude: Float, + cellLayout: CellLayout, + private val shakeAnimators: ArrayMap> +) : ValueAnimator.AnimatorUpdateListener where T : View, T : Reorderable { + + private var finalDeltaX = 0f + private var finalDeltaY = 0f + private var initDeltaX = + child.getTranslateDelegate().getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).value + private var initDeltaY = + child.getTranslateDelegate().getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).value + private var initScale = child.getReorderBounceScale() + private val finalScale = CellLayout.DEFAULT_SCALE - CHILD_DIVIDEND / child.width * initScale + + private val dir = if (mode == MODE_HINT) -1 else 1 + var animator: ValueAnimator = + ObjectAnimator.ofFloat(0f, 1f).also { + it.addUpdateListener(this) + it.setDuration((if (mode == MODE_HINT) HINT_DURATION else PREVIEW_DURATION).toLong()) + it.startDelay = (Math.random() * 60).toLong() + // Animations are disabled in power save mode, causing the repeated animation to jump + // spastically between beginning and end states. Since this looks bad, we don't repeat + // the animation in power save mode. + if (areAnimatorsEnabled() && mode == MODE_PREVIEW) { + it.repeatCount = ValueAnimator.INFINITE + it.repeatMode = ValueAnimator.REVERSE + } + } + + init { + val tmpRes = intArrayOf(0, 0) + cellLayout.regionToCenterPoint(cellX0, cellY0, spanX, spanY, tmpRes) + val (x0, y0) = tmpRes + cellLayout.regionToCenterPoint(cellX1, cellY1, spanX, spanY, tmpRes) + val (x1, y1) = tmpRes + val dX = x1 - x0 + val dY = y1 - y0 + + if (dX != 0 || dY != 0) { + if (dY == 0) { + finalDeltaX = -dir * sign(dX.toFloat()) * reorderMagnitude + } else if (dX == 0) { + finalDeltaY = -dir * sign(dY.toFloat()) * reorderMagnitude + } else { + val angle = atan((dY.toFloat() / dX)) + finalDeltaX = (-dir * sign(dX.toFloat()) * abs(cos(angle) * reorderMagnitude)) + finalDeltaY = (-dir * sign(dY.toFloat()) * abs(sin(angle) * reorderMagnitude)) + } + } + } + + private fun setInitialAnimationValuesToBaseline() { + initScale = CellLayout.DEFAULT_SCALE + initDeltaX = 0f + initDeltaY = 0f + } + + fun animate() { + val noMovement = finalDeltaX == 0f && finalDeltaY == 0f + if (shakeAnimators.containsKey(child)) { + val oldAnimation: ReorderPreviewAnimation? = shakeAnimators.remove(child) + if (noMovement) { + // A previous animation for this item exists, and no new animation will exist. + // Finish the old animation smoothly. + oldAnimation!!.finishAnimation() + return + } else { + // A previous animation for this item exists, and a new one will exist. Stop + // the old animation in its tracks, and proceed with the new one. + oldAnimation!!.cancel() + } + } + if (noMovement) { + return + } + shakeAnimators[child] = this + animator.start() + } + + override fun onAnimationUpdate(updatedAnimation: ValueAnimator) { + val progress = updatedAnimation.animatedValue as Float + child + .getTranslateDelegate() + .setTranslation( + INDEX_REORDER_BOUNCE_OFFSET, + /* x = */ progress * finalDeltaX + (1 - progress) * initDeltaX, + /* y = */ progress * finalDeltaY + (1 - progress) * initDeltaY + ) + child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale) + } + + private fun cancel() { + animator.cancel() + } + + /** Smoothly returns the item to its baseline position / scale */ + @Thunk + fun finishAnimation() { + animator.cancel() + setInitialAnimationValuesToBaseline() + animator = ObjectAnimator.ofFloat((animator.animatedValue as Float), 0f) + animator.addUpdateListener(this) + animator.interpolator = DECELERATE_1_5 + animator.setDuration(REORDER_ANIMATION_DURATION.toLong()) + animator.start() + } + + companion object { + const val PREVIEW_DURATION = 300 + const val HINT_DURATION = Workspace.REORDER_TIMEOUT + private const val CHILD_DIVIDEND = 4.0f + const val MODE_HINT = 0 + const val MODE_PREVIEW = 1 + } +} diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java index d37b1f08e3..5f786a4a68 100644 --- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java +++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java @@ -110,9 +110,6 @@ public class AccessibilityManagerCompat { } public static int getRecommendedTimeoutMillis(Context context, int originalTimeout, int flags) { - if (Utilities.ATLEAST_Q) { - return getManager(context).getRecommendedTimeoutMillis(originalTimeout, flags); - } - return originalTimeout; + return getManager(context).getRecommendedTimeoutMillis(originalTimeout, flags); } } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index e1816cb1e5..262b2a1e43 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -17,20 +17,28 @@ package com.android.launcher3.config; import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE; +import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS; import static com.android.launcher3.config.FeatureFlags.FlagState.DISABLED; import static com.android.launcher3.config.FeatureFlags.FlagState.ENABLED; import static com.android.launcher3.config.FeatureFlags.FlagState.TEAMFOOD; import static com.android.launcher3.uioverrides.flags.FlagsFactory.getDebugFlag; -import static com.android.launcher3.uioverrides.flags.FlagsFactory.getIntFlag; import static com.android.launcher3.uioverrides.flags.FlagsFactory.getReleaseFlag; import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; +import android.content.res.Resources; import android.view.ViewConfiguration; import androidx.annotation.VisibleForTesting; import com.android.launcher3.BuildConfig; import com.android.launcher3.Flags; +import com.android.launcher3.uioverrides.flags.FlagsFactory; import java.util.function.Predicate; import java.util.function.ToIntFunction; @@ -93,6 +101,12 @@ public final class FeatureFlags { "ENABLE_DISMISS_PREDICTION_UNDO", DISABLED, "Show an 'Undo' snackbar when users dismiss a predicted hotseat item"); + public static final BooleanFlag MOVE_STARTUP_DATA_TO_DEVICE_PROTECTED_STORAGE = getDebugFlag( + 251502424, "ENABLE_BOOT_AWARE_STARTUP_DATA", DISABLED, + "Marks LauncherPref data as (and allows it to) available while the device is" + + " locked. Enabling this causes a 1-time movement of certain SharedPreferences" + + " data. Improves startup latency."); + public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag(270395171, "CONTINUOUS_VIEW_TREE_CAPTURE", ENABLED, "Capture View tree every frame"); @@ -129,12 +143,14 @@ public final class FeatureFlags { "Shrinks navbar when long pressing if ANIMATE_LPNH is enabled"); public static final IntFlag LPNH_SLOP_PERCENTAGE = - getIntFlag(301680992, "LPNH_SLOP_PERCENTAGE", 100, - "Controls touch slop percentage for lpnh"); + FlagsFactory.getIntFlag(301680992, "LPNH_SLOP_PERCENTAGE", 100, + "Controls touch slop percentage for lpnh", + LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE); public static final IntFlag LPNH_TIMEOUT_MS = - getIntFlag(301680992, "LPNH_TIMEOUT_MS", ViewConfiguration.getLongPressTimeout(), - "Controls lpnh timeout in milliseconds"); + FlagsFactory.getIntFlag(301680992, "LPNH_TIMEOUT_MS", + ViewConfiguration.getLongPressTimeout(), + "Controls lpnh timeout in milliseconds", LONG_PRESS_NAV_HANDLE_TIMEOUT_MS); public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = getReleaseFlag( 270394468, "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", ENABLED, @@ -142,7 +158,7 @@ public final class FeatureFlags { // TODO(Block 5): Clean up flags public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851, - "ENABLE_TWOLINE_DEVICESEARCH", ENABLED, + "ENABLE_TWOLINE_DEVICESEARCH", DISABLED, "Enable two line label for icons with labels on device search."); public static final BooleanFlag ENABLE_ICON_IN_TEXT_HEADER = getDebugFlag(270395143, @@ -169,10 +185,6 @@ public final class FeatureFlags { // TODO(Block 8): Clean up flags // TODO(Block 9): Clean up flags - public static final BooleanFlag UNFOLDED_WIDGET_PICKER = getDebugFlag(301918659, - "UNFOLDED_WIDGET_PICKER", DISABLED, "Enable new widget picker that takes " - + "advantage of the unfolded foldable format"); - public static final BooleanFlag MULTI_SELECT_EDIT_MODE = getDebugFlag(270709220, "MULTI_SELECT_EDIT_MODE", DISABLED, "Enable new multi-select edit mode " + "for home screen"); @@ -188,11 +200,6 @@ public final class FeatureFlags { "ENABLE_SMARTSPACE_REMOVAL", DISABLED, "Enable SmartSpace removal for " + "home screen"); - // TODO(Block 10): Clean up flags - public static final BooleanFlag ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION = getDebugFlag(270614790, - "ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION", DISABLED, - "Enables predictive back animation from all apps and widgets to home"); - // TODO(Block 11): Clean up flags public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274, "FOLDABLE_SINGLE_PAGE", DISABLED, "Use a single page for the workspace"); @@ -201,10 +208,6 @@ public final class FeatureFlags { "ENABLE_PARAMETRIZE_REORDER", DISABLED, "Enables generating the reorder using a set of parameters"); - public static final BooleanFlag ENABLE_NO_LONG_PRESS_DRAG = getDebugFlag(299748096, - "ENABLE_NO_LONG_PRESS_DRAG", ENABLED, - "Don't trigger the drag if we are still under long press"); - // TODO(Block 12): Clean up flags public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680, "ENABLE_MULTI_INSTANCE", DISABLED, @@ -220,7 +223,19 @@ public final class FeatureFlags { TEAMFOOD, "Sends a notification whenever launcher encounters an uncaught exception."); public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION = - enableTaskbarNavbarUnification(); + enableTaskbarNavbarUnification() && !isPhone(); + + private static boolean isPhone() { + final boolean isPhone; + int foldedDeviceStatesId = Resources.getSystem().getIdentifier( + "config_foldedDeviceStates", "array", "android"); + if (foldedDeviceStatesId != 0) { + isPhone = Resources.getSystem().getIntArray(foldedDeviceStatesId).length == 0; + } else { + isPhone = true; + } + return isPhone; + } // Aconfig migration complete for ENABLE_TASKBAR_NO_RECREATION. public static final BooleanFlag ENABLE_TASKBAR_NO_RECREATION = getDebugFlag(299193589, @@ -231,7 +246,7 @@ public final class FeatureFlags { // Task bar pinning and task bar nav bar unification are both dependent on // ENABLE_TASKBAR_NO_RECREATION. We want to turn ENABLE_TASKBAR_NO_RECREATION on // when either of the dependent features is turned on. - || ENABLE_TASKBAR_PINNING.get() || ENABLE_TASKBAR_NAVBAR_UNIFICATION; + || enableTaskbarPinning() || ENABLE_TASKBAR_NAVBAR_UNIFICATION; } // TODO(Block 16): Clean up flags @@ -259,10 +274,7 @@ public final class FeatureFlags { // Aconfig migration complete for ENABLE_TWOLINE_ALLAPPS. public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937, - "ENABLE_TWOLINE_ALLAPPS", ENABLED, "Enables two line label inside all apps."); - public static boolean enableTwolineAllapps() { - return ENABLE_TWOLINE_ALLAPPS.get() || Flags.enableTwolineAllapps(); - } + "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps."); public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693, "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps"); @@ -276,7 +288,7 @@ public final class FeatureFlags { "Inject fallback app corpus result when AiAi fails to return it."); public static final BooleanFlag ENABLE_LONG_PRESS_NAV_HANDLE = - getReleaseFlag(299682306, "ENABLE_LONG_PRESS_NAV_HANDLE", TEAMFOOD, + getReleaseFlag(299682306, "ENABLE_LONG_PRESS_NAV_HANDLE", ENABLED, "Enables long pressing on the bottom bar nav handle to trigger events."); public static final BooleanFlag ENABLE_SEARCH_HAPTIC_HINT = @@ -288,32 +300,39 @@ public final class FeatureFlags { "Enables haptic hint at end of long pressing on the bottom bar nav handle."); public static final IntFlag LPNH_HAPTIC_HINT_START_SCALE_PERCENT = - getIntFlag(309972570, "LPNH_HAPTIC_HINT_START_SCALE_PERCENT", 0, - "Haptic hint start scale."); + FlagsFactory.getIntFlag(309972570, + "LPNH_HAPTIC_HINT_START_SCALE_PERCENT", 0, + "Haptic hint start scale.", + LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT); public static final IntFlag LPNH_HAPTIC_HINT_END_SCALE_PERCENT = - getIntFlag(309972570, "LPNH_HAPTIC_HINT_END_SCALE_PERCENT", 100, - "Haptic hint end scale."); + FlagsFactory.getIntFlag(309972570, + "LPNH_HAPTIC_HINT_END_SCALE_PERCENT", 100, + "Haptic hint end scale.", LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT); public static final IntFlag LPNH_HAPTIC_HINT_SCALE_EXPONENT = - getIntFlag(309972570, "LPNH_HAPTIC_HINT_SCALE_EXPONENT", 1, - "Haptic hint scale exponent."); + FlagsFactory.getIntFlag(309972570, + "LPNH_HAPTIC_HINT_SCALE_EXPONENT", 1, + "Haptic hint scale exponent.", + LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT); public static final IntFlag LPNH_HAPTIC_HINT_ITERATIONS = - getIntFlag(309972570, "LPNH_HAPTIC_HINT_ITERATIONS", 50, - "Haptic hint number of iterations."); + FlagsFactory.getIntFlag(309972570, "LPNH_HAPTIC_HINT_ITERATIONS", + 50, + "Haptic hint number of iterations.", + LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS); public static final BooleanFlag ENABLE_LPNH_DEEP_PRESS = getReleaseFlag(310952290, "ENABLE_LPNH_DEEP_PRESS", ENABLED, "Long press of nav handle is instantly triggered if deep press is detected."); public static final IntFlag LPNH_HAPTIC_HINT_DELAY = - getIntFlag(309972570, "LPNH_HAPTIC_HINT_DELAY", 0, - "Delay before haptic hint starts."); + FlagsFactory.getIntFlag(309972570, "LPNH_HAPTIC_HINT_DELAY", 0, + "Delay before haptic hint starts.", LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY); // TODO(Block 17): Clean up flags // Aconfig migration complete for ENABLE_TASKBAR_PINNING. - private static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(270396583, + private static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(296231746, "ENABLE_TASKBAR_PINNING", TEAMFOOD, "Enables taskbar pinning to allow user to switch between transient and persistent " + "taskbar flavors"); @@ -322,20 +341,6 @@ public final class FeatureFlags { return ENABLE_TASKBAR_PINNING.get() || Flags.enableTaskbarPinning(); } - /** - * Use a static boolean to gate the taskbar pinning education step - */ - public static boolean enableTaskbarPinningEdu() { - boolean enableTaskbarPinningEdu = false; - return enableTaskbarPinning() && enableTaskbarPinningEdu; - } - - public static final BooleanFlag MOVE_STARTUP_DATA_TO_DEVICE_PROTECTED_STORAGE = getDebugFlag( - 251502424, "ENABLE_BOOT_AWARE_STARTUP_DATA", DISABLED, - "Marks LauncherPref data as (and allows it to) available while the device is" - + " locked. Enabling this causes a 1-time movement of certain SharedPreferences" - + " data. Improves startup latency."); - // Aconfig migration complete for ENABLE_APP_PAIRS. public static final BooleanFlag ENABLE_APP_PAIRS = getDebugFlag(274189428, "ENABLE_APP_PAIRS", DISABLED, @@ -395,10 +400,6 @@ public final class FeatureFlags { "ENABLE_NEW_MIGRATION_LOGIC", ENABLED, "Enable the new grid migration logic, keeping pages when src < dest"); - public static final BooleanFlag ENABLE_CACHED_WIDGET = getDebugFlag(270395008, - "ENABLE_CACHED_WIDGET", ENABLED, - "Show previously cached widgets as opposed to deferred widget where available"); - // TODO(Block 25): Clean up flags public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257, "ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED, @@ -433,10 +434,6 @@ public final class FeatureFlags { "ENABLE_ENFORCED_ROUNDED_CORNERS", ENABLED, "Enforce rounded corners on all App Widgets"); - public static final BooleanFlag ENABLE_ICON_LABEL_AUTO_SCALING = getDebugFlag(270393294, - "ENABLE_ICON_LABEL_AUTO_SCALING", ENABLED, - "Enables scaling/spacing for icon labels to make more characters visible"); - public static final BooleanFlag USE_LOCAL_ICON_OVERRIDES = getDebugFlag(270394973, "USE_LOCAL_ICON_OVERRIDES", ENABLED, "Use inbuilt monochrome icons if app doesn't provide one"); @@ -484,10 +481,10 @@ public final class FeatureFlags { // TODO(Block 33): Clean up flags public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355, - "ENABLE_ALL_APPS_RV_PREINFLATION", TEAMFOOD, + "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED, "Enables preinflating all apps icons to avoid scrolling jank."); public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514, - "ALL_APPS_GONE_VISIBILITY", TEAMFOOD, + "ALL_APPS_GONE_VISIBILITY", ENABLED, "Set all apps container view's hidden visibility to GONE instead of INVISIBLE."); // TODO(Block 34): Empty block diff --git a/src/com/android/launcher3/dot/DotInfo.java b/src/com/android/launcher3/dot/DotInfo.java index fc180d1c17..64864b01cd 100644 --- a/src/com/android/launcher3/dot/DotInfo.java +++ b/src/com/android/launcher3/dot/DotInfo.java @@ -18,7 +18,6 @@ package com.android.launcher3.dot; import androidx.annotation.NonNull; -import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.notification.NotificationKeyData; import java.util.ArrayList; @@ -32,8 +31,7 @@ public class DotInfo { public static final int MAX_COUNT = 999; /** - * The keys of the notifications that this dot represents. These keys can later be - * used to retrieve {@link NotificationInfo}'s. + * The keys of the notifications that this dot represents. */ private final List mNotificationKeys = new ArrayList<>(); diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index 777f4d5686..b6e5977b87 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -16,8 +16,7 @@ package com.android.launcher3.dragndrop; -import static com.android.launcher3.Utilities.ATLEAST_Q; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NO_LONG_PRESS_DRAG; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; import android.graphics.Point; import android.graphics.Rect; @@ -32,8 +31,10 @@ import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; +import com.android.launcher3.Flags; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.ActivityContext; @@ -224,6 +225,12 @@ public abstract class DragController } } + protected boolean isItemPinnable() { + return !Flags.privateSpaceRestrictItemDrag() + || !(mDragObject.dragInfo instanceof ItemInfoWithIcon itemInfoWithIcon) + || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0; + } + public Optional getLogInstanceId() { return Optional.ofNullable(mDragObject) .map(dragObject -> dragObject.logInstanceId); @@ -405,9 +412,7 @@ public abstract class DragController mMotionDown.set(dragLayerPos.x, dragLayerPos.y); } - if (ATLEAST_Q) { - mLastTouchClassification = ev.getClassification(); - } + mLastTouchClassification = ev.getClassification(); return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev); } @@ -442,7 +447,7 @@ public abstract class DragController mLastTouch.set(x, y); int distanceDragged = mDistanceSinceScroll; - if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) { + if (mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) { distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR; } if (mIsInPreDrag && mOptions.preDragCondition != null @@ -464,7 +469,7 @@ public abstract class DragController private DropTarget checkTouchMove(final int x, final int y) { // If we are in predrag, don't trigger any other event until we get out of it - if (ENABLE_NO_LONG_PRESS_DRAG.get() && mIsInPreDrag) { + if (mIsInPreDrag) { return mLastDropTarget; } DropTarget dropTarget = findDropTarget(x, y); diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index f18f900593..db693f0fa9 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -42,6 +42,8 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; +import androidx.annotation.Nullable; + import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DropTargetBar; @@ -388,7 +390,13 @@ public class DragLayer extends BaseDragLayer implements LauncherOverla mDropAnim.start(); } - public void clearAnimatedView() { + /** + * Remove the drop view and end the drag animation. + * + * @return {@link DragView} that is removed. + */ + @Nullable + public DragView clearAnimatedView() { if (mDropAnim != null) { mDropAnim.cancel(); } @@ -396,8 +404,10 @@ public class DragLayer extends BaseDragLayer implements LauncherOverla if (mDropView != null) { mDragController.onDeferredEndDrag(mDropView); } + DragView ret = mDropView; mDropView = null; invalidate(); + return ret; } public View getAnimatedView() { diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index 1f0735243d..bcee4420b1 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -30,6 +30,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; +import android.appwidget.AppWidgetHostView; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -563,6 +564,11 @@ public abstract class DragView extends Fram return mContentViewParent; } + /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */ + public boolean containsAppWidgetHostView() { + return mContent instanceof AppWidgetHostView; + } + private static class SpringFloatValue { private static final FloatPropertyCompat VALUE = diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java index 6f295e6c50..6a43b24366 100644 --- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java +++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java @@ -37,7 +37,6 @@ 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.icons.BitmapRenderer; @@ -74,13 +73,9 @@ public class FolderAdaptiveIcon extends AdaptiveIconDrawable { return mBadge; } - @TargetApi(Build.VERSION_CODES.P) public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon( ActivityContext activity, int folderId, Point size) { Preconditions.assertNonUiThread(); - if (!Utilities.ATLEAST_P) { - return null; - } // assume square if (size.x != size.y) { diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java index da6f446725..f3708a2af6 100644 --- a/src/com/android/launcher3/dragndrop/LauncherDragController.java +++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java @@ -149,9 +149,10 @@ public class LauncherDragController extends DragController { handleMoveEvent(mLastTouch.x, mLastTouch.y); - if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) { + if (!isItemPinnable() + || (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null)) { // If it is an internal drag and the touch is already complete, cancel immediately - MAIN_EXECUTOR.submit(this::cancelDrag); + MAIN_EXECUTOR.post(this::cancelDrag); } return dragView; } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 084f829884..ecb5c8f186 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -40,6 +40,7 @@ import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.os.Looper; import android.text.InputType; import android.text.Selection; import android.text.TextUtils; @@ -165,10 +166,10 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo private static final Rect sTempRect = new Rect(); private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10; - private final Alarm mReorderAlarm = new Alarm(); - private final Alarm mOnExitAlarm = new Alarm(); - private final Alarm mOnScrollHintAlarm = new Alarm(); - final Alarm mScrollPauseAlarm = new Alarm(); + private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper()); + private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper()); + private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper()); + final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper()); final ArrayList mItemsInReadingOrder = new ArrayList(); @@ -297,10 +298,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mFooter = findViewById(R.id.folder_footer); mFooterHeight = dp.folderFooterHeightPx; - if (Utilities.ATLEAST_R) { - mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this); - setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback); - } + mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this); + setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback); } public boolean onLongClick(View v) { @@ -322,8 +321,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mDragController.addDragListener(new AccessibleDragListenerAdapter( mContent, FolderAccessibilityHelper::new) { @Override - protected void enableAccessibleDrag(boolean enable) { - super.enableAccessibleDrag(enable); + protected void enableAccessibleDrag(boolean enable, + @Nullable DragObject dragObject) { + super.enableAccessibleDrag(enable, dragObject); mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : IMPORTANT_FOR_ACCESSIBILITY_AUTO); @@ -422,18 +422,16 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Override public WindowInsets onApplyWindowInsets(WindowInsets windowInsets) { - if (Utilities.ATLEAST_R) { - this.setTranslationY(0); + this.setTranslationY(0); - if (windowInsets.isVisible(WindowInsets.Type.ime())) { - Insets keyboardInsets = windowInsets.getInsets(WindowInsets.Type.ime()); - int folderHeightFromBottom = getHeightFromBottom(); + if (windowInsets.isVisible(WindowInsets.Type.ime())) { + Insets keyboardInsets = windowInsets.getInsets(WindowInsets.Type.ime()); + int folderHeightFromBottom = getHeightFromBottom(); - if (keyboardInsets.bottom > folderHeightFromBottom) { - // Translate this folder above the keyboard, then add the folder name's padding - this.setTranslationY(folderHeightFromBottom - keyboardInsets.bottom - - mFolderName.getPaddingBottom()); - } + if (keyboardInsets.bottom > folderHeightFromBottom) { + // Translate this folder above the keyboard, then add the folder name's padding + this.setTranslationY(folderHeightFromBottom - keyboardInsets.bottom + - mFolderName.getPaddingBottom()); } } @@ -804,6 +802,14 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return; } + int size = getIconsInReadingOrder().size(); + if (size <= 1) { + Log.d(TAG, "Couldn't animate folder closed because there's " + size + " icons"); + closeComplete(false); + post(this::announceAccessibilityChanges); + return; + } + mContent.completePendingPageChanges(); mContent.snapToPageImmediately(mContent.getDestinationPage()); @@ -812,15 +818,13 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - if (Utilities.ATLEAST_R) { - setWindowInsetsAnimationCallback(null); - } + setWindowInsetsAnimationCallback(null); mIsAnimatingClosed = true; } @Override public void onAnimationEnd(Animator animation) { - if (Utilities.ATLEAST_R && mKeyboardInsetAnimationCallback != null) { + if (mKeyboardInsetAnimationCallback != null) { setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback); } closeComplete(true); @@ -1035,6 +1039,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public void onDropCompleted(final View target, final DragObject d, final boolean success) { if (success) { + if (getItemCount() <= 1) { + mDeleteFolderOnDropCompleted = true; + } if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) { replaceFolderWithFinalItem(); } diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index f058ae4d02..ee0d5fce24 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -29,8 +29,10 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Looper; import android.util.AttributeSet; import android.util.Property; import android.view.LayoutInflater; @@ -120,7 +122,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel boolean mAnimating = false; - private Alarm mOpenAlarm = new Alarm(); + private Alarm mOpenAlarm = new Alarm(Looper.getMainLooper()); private boolean mForceHideDot; @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) @@ -633,6 +635,20 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + boolean shouldCenterIcon = mActivity.getDeviceProfile().iconCenterVertically; + if (shouldCenterIcon) { + int iconSize = mActivity.getDeviceProfile().iconSizePx; + Paint.FontMetrics fm = mFolderName.getPaint().getFontMetrics(); + int cellHeightPx = iconSize + mFolderName.getCompoundDrawablePadding() + + (int) Math.ceil(fm.bottom - fm.top); + setPadding(getPaddingLeft(), (MeasureSpec.getSize(heightMeasureSpec) + - cellHeightPx) / 2, getPaddingRight(), getPaddingBottom()); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + /** Sets the visibility of the icon's title text */ public void setTextVisible(boolean visible) { if (visible) { diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java index 66c9109b82..78298b3a14 100644 --- a/src/com/android/launcher3/folder/LauncherDelegate.java +++ b/src/com/android/launcher3/folder/LauncherDelegate.java @@ -94,7 +94,8 @@ public class LauncherDelegate { CellLayout cellLayout = mLauncher.getCellLayout(info.container, mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId); finalItem = info.contents.remove(0); - newIcon = mLauncher.createShortcut(cellLayout, finalItem); + newIcon = mLauncher.getItemInflater().inflateItem( + finalItem, mLauncher.getModelWriter(), cellLayout); mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem, info.container, info.screenId, info.cellX, info.cellY); } diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java index b39e968e63..9001a0c271 100644 --- a/src/com/android/launcher3/folder/PreviewItemManager.java +++ b/src/com/android/launcher3/folder/PreviewItemManager.java @@ -37,6 +37,7 @@ import android.util.FloatProperty; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Utilities; @@ -71,7 +72,8 @@ public class PreviewItemManager { private final Context mContext; private final FolderIcon mIcon; - private final int mIconSize; + @VisibleForTesting + public final int mIconSize; // These variables are all associated with the drawing of the preview; they are stored // as member variables for shared usage and to avoid computation on each frame @@ -117,7 +119,7 @@ public class PreviewItemManager { final Runnable onCompleteRunnable) { return reverse ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1, - FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable) + FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable) : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2, INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable); } @@ -217,9 +219,9 @@ public class PreviewItemManager { /** * Draws each preview item. * - * @param offset The offset needed to draw the preview items. + * @param offset The offset needed to draw the preview items. * @param shouldClipPath Iff true, clip path using {@param clipPath}. - * @param clipPath The clip path of the folder icon. + * @param clipPath The clip path of the folder icon. */ private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset, boolean shouldClipPath, Path clipPath) { @@ -360,13 +362,13 @@ public class PreviewItemManager { /** * Handles the case where items in the preview are either: - * - Moving into the preview - * - Moving into a new position - * - Moving out of the preview + * - Moving into the preview + * - Moving into a new position + * - Moving out of the preview * * @param oldItems The list of items in the old preview. * @param newItems The list of items in the new preview. - * @param dropped The item that was dropped onto the FolderIcon. + * @param dropped The item that was dropped onto the FolderIcon. */ public void onDrop(List oldItems, List newItems, WorkspaceItemInfo dropped) { @@ -428,11 +430,11 @@ public class PreviewItemManager { p.anim = anim; } - private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) { + @VisibleForTesting + public void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) { if (item.hasPromiseIconUi() || (item.runtimeStatusFlags - & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { + & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { PreloadIconDrawable drawable = newPendingIcon(mContext, item); - drawable.setLevel(item.getProgressLevel()); p.drawable = drawable; } else { p.drawable = item.newIcon(mContext, @@ -440,7 +442,6 @@ public class PreviewItemManager { } p.drawable.setBounds(0, 0, mIconSize, mIconSize); p.item = item; - // Set the callback to FolderIcon as it is responsible to drawing the icon. The // callback will be released when the folder is opened. p.drawable.setCallback(mIcon); diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java index 18200f6447..dc8694d2f6 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java @@ -19,7 +19,6 @@ import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.Themes.isThemedIconEnabled; -import android.annotation.TargetApi; import android.content.ContentProvider; import android.content.ContentValues; import android.content.pm.PackageManager; @@ -27,7 +26,6 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.os.Binder; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -41,7 +39,6 @@ import android.util.Pair; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.InvariantDeviceProfile.GridOption; import com.android.launcher3.LauncherPrefs; -import com.android.launcher3.Utilities; import com.android.launcher3.util.Executors; /** @@ -184,13 +181,12 @@ public class GridCustomizationsProvider extends ContentProvider { return null; } - if (!Utilities.ATLEAST_R || !METHOD_GET_PREVIEW.equals(method)) { + if (!METHOD_GET_PREVIEW.equals(method)) { return null; } return getPreview(extras); } - @TargetApi(Build.VERSION_CODES.R) private synchronized Bundle getPreview(Bundle request) { PreviewLifecycleObserver observer = null; try { diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 3330448919..e0a66276b6 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -26,7 +26,6 @@ import static com.android.launcher3.config.FeatureFlags.shouldShowFirstPageWidge import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks; -import android.annotation.TargetApi; import android.app.Fragment; import android.app.WallpaperColors; import android.app.WallpaperManager; @@ -35,10 +34,10 @@ import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.ContextWrapper; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.PointF; import android.graphics.Rect; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; @@ -120,7 +119,6 @@ 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.R) public class LauncherPreviewRenderer extends ContextWrapper implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 { @@ -197,7 +195,7 @@ public class LauncherPreviewRenderer extends ContextWrapper mUiHandler = new Handler(Looper.getMainLooper()); mContext = context; mIdp = idp; - mDp = idp.getDeviceProfile(context).toBuilder(context).setViewScaleProvider( + mDp = getDeviceProfileForPreview(context).toBuilder(context).setViewScaleProvider( this::getAppWidgetScale).build(); if (context instanceof PreviewContext) { Context tempContext = ((PreviewContext) context).getBaseContext(); @@ -259,6 +257,21 @@ public class LauncherPreviewRenderer extends ContextWrapper mAppWidgetHost = new LauncherPreviewAppWidgetHost(context); } + /** + * Returns the device profile based on resource configuration for previewing various display + * sizes + */ + private DeviceProfile getDeviceProfileForPreview(Context context) { + float density = context.getResources().getDisplayMetrics().density; + Configuration config = context.getResources().getConfiguration(); + + return mIdp.getBestMatch( + config.screenWidthDp * density, + config.screenHeightDp * density, + WindowManagerProxy.INSTANCE.get(context).getRotation(context) + ); + } + /** * Returns the insets of the screen closest to the display given by the context */ diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java index 3e77c78f98..9fffcc1bd2 100644 --- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -173,6 +173,8 @@ public class PreloadIconDrawable extends FastBitmapDrawable { mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1); setLevel(info.getProgressLevel()); + // Set a disabled icon color if the app is suspended or if the app is pending download + setIsDisabled(info.isDisabled() || info.isPendingDownload()); } @Override diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index 683354bf45..051fb6f4fe 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -49,7 +49,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; -import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext; import com.android.launcher3.model.BgDataModel; @@ -211,10 +210,7 @@ public class PreviewSurfaceRenderer { return new ContextThemeWrapper(context, Themes.getActivityThemeRes(context)); } - if (Utilities.ATLEAST_R) { - context = context.createWindowContext( - LayoutParams.TYPE_APPLICATION_OVERLAY, null); - } + context = context.createWindowContext(LayoutParams.TYPE_APPLICATION_OVERLAY, null); LocalColorExtractor.newInstance(context) .applyColorsOverride(context, mWallpaperColors); return new ContextThemeWrapper(context, @@ -259,7 +255,7 @@ public class PreviewSurfaceRenderer { query += " or " + LauncherSettings.Favorites.SCREEN + " = " + Workspace.SECOND_SCREEN_ID; } - loadWorkspace(new ArrayList<>(), query, null); + loadWorkspace(new ArrayList<>(), query, null, null); final SparseArray spanInfo = getLoadedLauncherWidgetInfo(previewContext.getBaseContext()); diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 2f7f51e6f7..8e73660ae0 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -37,6 +37,7 @@ import android.content.pm.ShortcutInfo; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.graphics.drawable.Drawable; +import android.os.Looper; import android.os.Process; import android.os.Trace; import android.os.UserHandle; @@ -44,6 +45,7 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -54,7 +56,6 @@ import com.android.launcher3.Utilities; import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic; import com.android.launcher3.icons.cache.BaseIconCache; import com.android.launcher3.icons.cache.CachingLogic; -import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; @@ -63,9 +64,9 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.util.Preconditions; import com.android.launcher3.widget.WidgetSections; import com.android.launcher3.widget.WidgetSections.WidgetSection; @@ -100,7 +101,7 @@ public class IconCache extends BaseIconCache { private final UserCache mUserManager; private final InstantAppResolver mInstantAppResolver; private final IconProvider mIconProvider; - private final HandlerRunnable mCancelledRunnable; + private final CancellableTask mCancelledTask; private final SparseArray mWidgetCategoryBitmapInfos; @@ -119,9 +120,8 @@ public class IconCache extends BaseIconCache { mIconProvider = iconProvider; mWidgetCategoryBitmapInfos = new SparseArray<>(); - mCancelledRunnable = new HandlerRunnable( - mWorkerHandler, () -> null, MAIN_EXECUTOR, c -> { }); - mCancelledRunnable.cancel(); + mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { }); + mCancelledTask.cancel(); } @Override @@ -174,9 +174,9 @@ public class IconCache extends BaseIconCache { * * @return a request ID that can be used to cancel the request. */ - public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller, + @AnyThread + public CancellableTask updateIconInBackground(final ItemInfoUpdateReceiver caller, final ItemInfoWithIcon info) { - Preconditions.assertUIThread(); Supplier task; if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) { task = () -> { @@ -191,16 +191,22 @@ public class IconCache extends BaseIconCache { } else { Log.i(TAG, "Icon update not supported for " + info == null ? "null" : info.getClass().getName()); - return mCancelledRunnable; + return mCancelledTask; } - if (mPendingIconRequestCount <= 0) { - MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); + Runnable endRunnable; + if (Looper.myLooper() == Looper.getMainLooper()) { + if (mPendingIconRequestCount <= 0) { + MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); + } + mPendingIconRequestCount++; + endRunnable = this::onIconRequestEnd; + } else { + endRunnable = () -> { }; } - mPendingIconRequestCount++; - HandlerRunnable request = new HandlerRunnable<>(mWorkerHandler, - task, MAIN_EXECUTOR, caller::reapplyItemInfo, this::onIconRequestEnd); + CancellableTask request = new CancellableTask<>( + task, MAIN_EXECUTOR, caller::reapplyItemInfo, endRunnable); Utilities.postAsyncCallback(mWorkerHandler, request); return request; } @@ -219,7 +225,19 @@ public class IconCache extends BaseIconCache { CacheEntry entry = cacheLocked(application.componentName, application.user, () -> null, mLauncherActivityInfoCachingLogic, false, application.usingLowResIcon()); - if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) { + if (entry.bitmap == null || isDefaultIcon(entry.bitmap, application.user)) { + return; + } + + boolean preferPackageIcon = application.isArchived(); + if (preferPackageIcon) { + String packageName = application.getTargetPackage(); + CacheEntry packageEntry = + cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + application.user, () -> null, mLauncherActivityInfoCachingLogic, + false, application.usingLowResIcon()); + applyPackageEntry(packageEntry, application, entry); + } else { applyCacheEntry(entry, application); } } @@ -227,10 +245,14 @@ public class IconCache extends BaseIconCache { /** * Fill in {@param info} with the icon and label for {@param activityInfo} */ + @SuppressWarnings("NewApi") public synchronized void getTitleAndIcon(ItemInfoWithIcon info, LauncherActivityInfo activityInfo, boolean useLowResIcon) { + boolean isAppArchived = Utilities.enableSupportForArchiving() && activityInfo != null + && activityInfo.getActivityInfo().isArchived; // If we already have activity info, no need to use package icon - getTitleAndIcon(info, () -> activityInfo, false, useLowResIcon); + getTitleAndIcon(info, () -> activityInfo, isAppArchived, useLowResIcon, + isAppArchived); } /** @@ -309,7 +331,7 @@ public class IconCache extends BaseIconCache { } else { Intent intent = info.getIntent(); getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user), - true, useLowResIcon); + true, useLowResIcon, info.isArchived()); } } @@ -333,6 +355,28 @@ public class IconCache extends BaseIconCache { applyCacheEntry(entry, infoInOut); } + /** + * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + @NonNull ItemInfoWithIcon infoInOut, + @NonNull Supplier activityInfoProvider, + boolean usePkgIcon, boolean useLowResIcon, boolean preferPackageEntry) { + CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user, + activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, + useLowResIcon); + if (preferPackageEntry) { + String packageName = infoInOut.getTargetPackage(); + CacheEntry packageEntry = cacheLocked( + new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + infoInOut.user, activityInfoProvider, mLauncherActivityInfoCachingLogic, + usePkgIcon, useLowResIcon); + applyPackageEntry(packageEntry, infoInOut, entry); + } else { + applyCacheEntry(entry, infoInOut); + } + } + /** * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles. * @@ -551,6 +595,19 @@ public class IconCache extends BaseIconCache { } } + protected void applyPackageEntry(@NonNull final CacheEntry packageEntry, + @NonNull final ItemInfoWithIcon info, @NonNull final CacheEntry fallbackEntry) { + info.title = Utilities.trim(packageEntry.title); + info.appTitle = Utilities.trim(fallbackEntry.title); + info.contentDescription = packageEntry.contentDescription; + info.bitmap = packageEntry.bitmap; + if (packageEntry.bitmap == null) { + // TODO: entry.bitmap can never be null, so this should not happen at all. + Log.wtf(TAG, "Cannot find bitmap from the cache, default icon was loaded."); + info.bitmap = getDefaultIcon(info.user); + } + } + public Drawable getFullResIcon(LauncherActivityInfo info) { return mIconProvider.getIcon(info, mIconDpi); } diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java index 83003ffb14..3e320bdb35 100644 --- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java +++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java @@ -20,7 +20,9 @@ import android.graphics.Rect; import android.view.View; import android.view.View.OnFocusChangeListener; +import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.util.Themes; /** * A helper class to draw background of a focused view. @@ -29,7 +31,9 @@ public abstract class FocusIndicatorHelper extends ItemFocusIndicatorHelper implements AnimatorUpdateListe mContainer = container; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mMaxAlpha = Color.alpha(color); mPaint.setColor(0xFF000000 | color); + if (Flags.enableFocusOutline()) { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(container.getResources().getDimensionPixelSize( + R.dimen.focus_outline_stroke_width)); + mRadius = container.getResources().getDimensionPixelSize( + R.dimen.focus_outline_radius); + } else { + mPaint.setStyle(Paint.Style.FILL); + mRadius = container.getResources().getDimensionPixelSize( + R.dimen.grid_visualization_rounding_radius); + } + mMaxAlpha = Color.alpha(color); setAlpha(0); mShift = 0; - mRadius = container.getResources().getDimensionPixelSize( - R.dimen.grid_visualization_rounding_radius); } protected void setAlpha(float alpha) { @@ -136,6 +146,13 @@ public abstract class ItemFocusIndicatorHelper implements AnimatorUpdateListe Rect newRect = getDrawRect(); if (newRect != null) { + if (Flags.enableFocusOutline()) { + // Stroke is drawn with half outside and half inside the view. Inset by half + // stroke width to move the whole stroke inside the view and avoid other views + // occluding it + int halfStrokeWidth = (int) mPaint.getStrokeWidth() / 2; + newRect.inset(halfStrokeWidth, halfStrokeWidth); + } mDirtyRect.set(newRect); c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top, (float) mDirtyRect.right, (float) mDirtyRect.bottom, diff --git a/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java b/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java index fde220cbf6..f9bd3437a5 100644 --- a/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java +++ b/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java @@ -50,10 +50,18 @@ public class ViewGroupFocusHelper extends FocusIndicatorHelper { } private void computeLocationRelativeToContainer(View child, Rect outRect) { - View parent = (View) child.getParent(); + if (child == null) { + return; + } + outRect.left += child.getX(); outRect.top += child.getY(); + if (child.getParent() == null || !(child.getParent() instanceof View)) { + return; + } + + View parent = (View) child.getParent(); if (parent != mContainer) { if (parent instanceof PagedView) { PagedView page = (PagedView) parent; diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index d8388c2709..5cb15406cc 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -117,6 +117,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "Task launched from overview using SWIPE DOWN") LAUNCHER_TASK_LAUNCH_SWIPE_DOWN(340), + @UiEvent(doc = "App launched by dragging and dropping, probably from taskbar") + LAUNCHER_APP_LAUNCH_DRAGDROP(1552), + @UiEvent(doc = "TASK dismissed from overview using SWIPE UP") LAUNCHER_TASK_DISMISS_SWIPE_UP(341), @@ -196,6 +199,11 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on app info system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP(515), + /** + * @deprecated Use {@link #LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP} or + * {@link #LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM} + */ + @Deprecated @UiEvent(doc = "User tapped on split screen icon on a task menu.") LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP(518), @@ -208,6 +216,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on pin system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP(522), + @UiEvent(doc = "User tapped on don't suggest app system shortcut.") + LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP(1603), + @UiEvent(doc = "User is shown All Apps education view.") LAUNCHER_ALL_APPS_EDU_SHOWN(523), @@ -281,6 +292,12 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User long presses on the bottom bezel area.") LAUNCHER_LONG_PRESS_NAVBAR(1544), + @UiEvent(doc = "User deep presses on the stashed taskbar") + LAUNCHER_DEEP_PRESS_STASHED_TASKBAR(1602), + + @UiEvent(doc = "User long presses on the stashed taskbar") + LAUNCHER_LONG_PRESS_STASHED_TASKBAR(1592), + @UiEvent(doc = "User swipes or fling in UP direction from bottom bazel area.") LAUNCHER_HOME_GESTURE(574), @@ -624,6 +641,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped taskbar All Apps button.") LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP(1057), + @UiEvent(doc = "User long pressed taskbar All Apps button.") + LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS(1607), + @UiEvent(doc = "User tapped on Share app system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP(1075), @@ -698,6 +718,39 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped private space settings button") LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP(1550), + @UiEvent(doc = "User tapped on install to private space system shortcut.") + LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP(1565), + + @UiEvent(doc = "User tapped private space install app button.") + LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP(1605), + + @UiEvent(doc = "User attempted to create split screen with a widget") + LAUNCHER_SPLIT_WIDGET_ATTEMPT(1604), + + @UiEvent(doc = "User tapped on private space uninstall system shortcut.") + LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP(1608), + + @UiEvent(doc = "User initiated split selection") + LAUNCHER_SPLIT_SELECTION_INITIATED(1618), + + @UiEvent(doc = "User finished a split selection session") + LAUNCHER_SPLIT_SELECTION_COMPLETE(1619), + + @UiEvent(doc = "User selected both apps for split screen") + LAUNCHER_SPLIT_SELECTED_SECOND_APP(1609), + + @UiEvent(doc = "User exited split selection by going home via swipe, button, or state " + + "transition") + LAUNCHER_SPLIT_SELECTION_EXIT_HOME(1610), + + @UiEvent(doc = "User exited split selection by tapping cancel in split instructions view") + LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON(1611), + + @UiEvent(doc = "User exited split selection when another activity/app came to foreground" + + " after first app had been selected OR if user long-pressed on home. Default exit" + + " metric.") + LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED(1612), + // ADD MORE ; diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java index 5e86bd6b8a..96a8da97f3 100644 --- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java +++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java @@ -33,6 +33,7 @@ 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.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemFactory; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -102,6 +103,11 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { Objects.requireNonNull(item.getIntent()))) { continue; } + + if (item instanceof ItemInfoWithIcon + && ((ItemInfoWithIcon) item).isArchived()) { + continue; + } } if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java index 190eb78d2a..8659471d27 100644 --- a/src/com/android/launcher3/model/AllAppsList.java +++ b/src/com/android/launcher3/model/AllAppsList.java @@ -18,6 +18,7 @@ package com.android.launcher3.model; import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import android.content.ComponentName; import android.content.Context; @@ -32,6 +33,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.AppFilter; +import com.android.launcher3.Utilities; import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.BgDataModel.Callbacks; @@ -53,6 +55,7 @@ import java.util.function.Predicate; /** * Stores the list of all applications for the all apps view. */ +@SuppressWarnings("NewApi") public class AllAppsList { private static final String TAG = "AllAppsList"; @@ -200,9 +203,16 @@ public class AllAppsList { if (tgtComp != null && tgtComp.getPackageName().equals(installInfo.packageName) && appInfo.user.equals(user)) { if (installInfo.state == PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING - || installInfo.state == PackageInstallInfo.STATUS_INSTALLING) { + || installInfo.state == PackageInstallInfo.STATUS_INSTALLING + // In case unarchival fails, we would want to keep the icon and update + // back the progress to 0 for the all apps view without removing the + // icon, which is contrary to what happens during normal app installation + // flow. + || (installInfo.state == PackageInstallInfo.STATUS_FAILED + && appInfo.isArchived())) { if (appInfo.isAppStartable() - && installInfo.state == PackageInstallInfo.STATUS_INSTALLING) { + && installInfo.state == PackageInstallInfo.STATUS_INSTALLING + && !appInfo.isArchived()) { continue; } appInfo.setProgressLevel(installInfo); @@ -320,7 +330,15 @@ public class AllAppsList { PackageManagerHelper.getLoadingProgress(info), PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); applicationInfo.intent = launchIntent; - + if (Utilities.enableSupportForArchiving()) { + // In case an app is archived, the respective item flag corresponding to + // archiving should also be applied during package updates + if (info.getActivityInfo().isArchived) { + applicationInfo.runtimeStatusFlags |= FLAG_ARCHIVED; + } else { + applicationInfo.runtimeStatusFlags &= (~FLAG_ARCHIVED); + } + } mDataChanged = true; } } diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java index 9b2344d2ad..fa2a1b01c7 100644 --- a/src/com/android/launcher3/model/BaseLauncherBinder.java +++ b/src/com/android/launcher3/model/BaseLauncherBinder.java @@ -16,20 +16,25 @@ package com.android.launcher3.model; +import static com.android.launcher3.Flags.enableWorkspaceInflation; import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL; import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.os.Process; import android.os.Trace; import android.util.Log; +import android.util.Pair; +import android.view.View; 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.Workspace; +import com.android.launcher3.celllayout.CellPosMapper; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.BgDataModel.FixedContainerItems; @@ -38,6 +43,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; +import com.android.launcher3.util.ItemInflater; import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.PackageUserKey; @@ -279,8 +285,8 @@ public abstract class BaseLauncherBinder { // Separate the items that are on the current screen, and all the other remaining items ArrayList currentWorkspaceItems = new ArrayList<>(); ArrayList otherWorkspaceItems = new ArrayList<>(); - ArrayList currentAppWidgets = new ArrayList<>(); - ArrayList otherAppWidgets = new ArrayList<>(); + ArrayList currentAppWidgets = new ArrayList<>(); + ArrayList otherAppWidgets = new ArrayList<>(); filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems, otherWorkspaceItems); @@ -304,8 +310,8 @@ public abstract class BaseLauncherBinder { executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); // Load items on the current page. - bindWorkspaceItems(currentWorkspaceItems, mUiExecutor); - bindAppWidgets(currentAppWidgets, mUiExecutor); + bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor); + bindItemsInChunks(currentAppWidgets, 1, mUiExecutor); if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) { mExtraItems.forEach(item -> executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor)); @@ -313,8 +319,41 @@ public abstract class BaseLauncherBinder { RunnableList pendingTasks = new RunnableList(); Executor pendingExecutor = pendingTasks::add; - bindWorkspaceItems(otherWorkspaceItems, pendingExecutor); - bindAppWidgets(otherAppWidgets, pendingExecutor); + + RunnableList onCompleteSignal = new RunnableList(); + + if (enableWorkspaceInflation()) { + MODEL_EXECUTOR.execute(() -> { + setupPendingBind(otherWorkspaceItems, otherAppWidgets, currentScreenIds, + pendingExecutor); + + // Wait for the async inflation to complete and then notify the completion + // signal on UI thread. + MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy); + }); + } else { + setupPendingBind( + otherWorkspaceItems, otherAppWidgets, currentScreenIds, pendingExecutor); + onCompleteSignal.executeAllAndDestroy(); + } + + executeCallbacksTask( + c -> { + if (!enableWorkspaceInflation()) { + MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + c.onInitialBindComplete(currentScreenIds, pendingTasks, onCompleteSignal, + workspaceItemCount, isBindSync); + }, mUiExecutor); + } + + private void setupPendingBind( + List otherWorkspaceItems, + List otherAppWidgets, + IntSet currentScreenIds, + Executor pendingExecutor) { + bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor); + bindItemsInChunks(otherAppWidgets, 1, pendingExecutor); StringCache cacheClone = mBgDataModel.stringCache.clone(); executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor); @@ -326,38 +365,51 @@ public abstract class BaseLauncherBinder { ItemInstallQueue.INSTANCE.get(mApp.getContext()) .resumeModelPush(FLAG_LOADER_RUNNING); }); - - executeCallbacksTask( - c -> { - MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - c.onInitialBindComplete( - currentScreenIds, pendingTasks, workspaceItemCount, isBindSync); - }, mUiExecutor); } - private void bindWorkspaceItems( - final ArrayList workspaceItems, final Executor executor) { + /** + * Tries to inflate the items asynchronously and bind. Returns true on success or false if + * async-binding is not supported in this case. + */ + private boolean inflateAsyncAndBind(List items, Executor executor) { + if (!enableWorkspaceInflation()) { + return false; + } + ItemInflater inflater = mCallbacks.getItemInflater(); + if (inflater == null) { + return false; + } + + if (mMyBindingId != mBgDataModel.lastBindId) { + Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation"); + return true; + } + + ModelWriter writer = mApp.getModel() + .getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null); + List> bindItems = items.stream().map(i -> + Pair.create(i, inflater.inflateItem(i, writer, null))).toList(); + executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor); + return true; + } + + private void bindItemsInChunks(List workspaceItems, int chunkCount, + Executor executor) { + if (inflateAsyncAndBind(workspaceItems, executor)) { + return; + } + // Bind the workspace items int count = workspaceItems.size(); - for (int i = 0; i < count; i += ITEMS_CHUNK) { + for (int i = 0; i < count; i += chunkCount) { final int start = i; - final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i); + final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i); executeCallbacksTask( c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), executor); } } - private void bindAppWidgets(List appWidgets, Executor executor) { - // Bind the widgets, one at a time - int count = appWidgets.size(); - for (int i = 0; i < count; i++) { - final ItemInfo widget = appWidgets.get(i); - executeCallbacksTask( - c -> c.bindItems(Collections.singletonList(widget), false), executor); - } - } - protected void executeCallbacksTask(CallbackTask task, Executor executor) { executor.execute(() -> { if (mMyBindingId != mBgDataModel.lastBindId) { @@ -430,8 +482,11 @@ public abstract class BaseLauncherBinder { bindAppWidgets(appWidgets); executeCallbacksTask(c -> { MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - c.onInitialBindComplete( - mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync); + + RunnableList onCompleteSignal = new RunnableList(); + onCompleteSignal.executeAllAndDestroy(); + c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal, + workspaceItemCount, isBindSync); }, mUiExecutor); } diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 7f0f683091..8579d1d682 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -33,6 +33,8 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -54,6 +56,7 @@ 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.ItemInflater; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.RunnableList; import com.android.launcher3.widget.model.WidgetsListBaseEntry; @@ -495,7 +498,15 @@ public class BgDataModel { default void clearPendingBinds() { } default void startBinding() { } - default void bindItems(List shortcuts, boolean forceAnimateIcons) { } + @Nullable + default ItemInflater getItemInflater() { + return null; + } + + default void bindItems(@NonNull List shortcuts, boolean forceAnimateIcons) { } + /** Alternate method to bind preinflated views */ + default void bindInflatedItems(@NonNull List> items) { } + default void bindScreens(IntArray orderedScreenIds) { } default void setIsFirstPagePinnedItemEnabled(boolean isFirstPagePinnedItemEnabled) { } default void finishBindingItems(IntSet pagesBoundFirst) { } @@ -520,7 +531,9 @@ public class BgDataModel { default void bindSmartspaceWidget() { } /** Called when workspace has been bound. */ - default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks, + default void onInitialBindComplete(@NonNull IntSet boundPages, + @NonNull RunnableList pendingTasks, + @NonNull RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) { pendingTasks.executeAllAndDestroy(); } diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java index efd5574519..af66431d47 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java +++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java @@ -262,7 +262,8 @@ public class GridSizeMigrationUtil { String srcTableName, String destTableName) { int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName); - if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { + if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER + || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) { for (Set itemIds : entry.mFolderItems.values()) { for (int itemId : itemIds) { copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName); diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java index 9a3abd47fd..d35087980a 100644 --- a/src/com/android/launcher3/model/ItemInstallQueue.java +++ b/src/com/android/launcher3/model/ItemInstallQueue.java @@ -22,6 +22,7 @@ import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICA import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.appwidget.AppWidgetManager; @@ -43,6 +44,7 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.Utilities; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -276,6 +278,7 @@ public class ItemInstallQueue { return intent; } + @SuppressWarnings("NewApi") public Pair getItemInfo(Context context) { switch (itemType) { case ITEM_TYPE_APPLICATION: { @@ -297,6 +300,10 @@ public class ItemInstallQueue { } else { lai = laiList.get(0); si.intent = makeLaunchIntent(lai); + if (Utilities.enableSupportForArchiving() + && lai.getActivityInfo().isArchived) { + si.runtimeStatusFlags |= FLAG_ARCHIVED; + } } LauncherAppState.getInstance(context).getIconCache() .getTitleAndIcon(si, () -> lai, usePackageIcon, false); diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 4370043569..2f678a858a 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -41,6 +41,8 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.FileLog; @@ -48,11 +50,13 @@ import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; 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.UserIconInfo; import java.net.URISyntaxException; import java.security.InvalidParameterException; @@ -70,6 +74,7 @@ public class LoaderCursor extends CursorWrapper { private final Context mContext; private final IconCache mIconCache; private final InvariantDeviceProfile mIDP; + private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger; private final IntArray mItemsToRemove = new IntArray(); private final IntArray mRestoredRows = new IntArray(); @@ -107,7 +112,8 @@ public class LoaderCursor extends CursorWrapper { public int itemType; public int restoreFlag; - public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState) { + public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState, + @Nullable LauncherRestoreEventLogger restoreEventLogger) { super(cursor); mApp = app; @@ -115,6 +121,7 @@ public class LoaderCursor extends CursorWrapper { mContext = app.getContext(); mIconCache = app.getIconCache(); mIDP = app.getInvariantDeviceProfile(); + mRestoreEventLogger = restoreEventLogger; // Init column indices mIconIndex = getColumnIndexOrThrow(Favorites.ICON); @@ -348,6 +355,8 @@ public class LoaderCursor extends CursorWrapper { final WorkspaceItemInfo info = new WorkspaceItemInfo(); info.user = user; info.intent = newIntent; + UserCache userCache = UserCache.getInstance(mContext); + UserIconInfo userIconInfo = userCache.getUserInfo(user); if (loadIcon) { mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon); @@ -357,7 +366,7 @@ public class LoaderCursor extends CursorWrapper { } if (mActivityInfo != null) { - AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo); + AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo); } // from the db @@ -390,9 +399,12 @@ public class LoaderCursor extends CursorWrapper { /** * Marks the current item for removal */ - public void markDeleted(String reason) { + public void markDeleted(String reason, @RestoreError String errorType) { FileLog.e(TAG, reason); mItemsToRemove.add(id); + if (mRestoreEventLogger != null) { + mRestoreEventLogger.logSingleFavoritesItemRestoreFailed(itemType, errorType); + } } /** @@ -431,6 +443,9 @@ public class LoaderCursor extends CursorWrapper { mApp.getModel().getModelDbController().update(TABLE_NAME, values, Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null); } + if (mRestoreEventLogger != null) { + mRestoreEventLogger.reportLauncherRestoreResults(); + } } /** @@ -473,8 +488,11 @@ public class LoaderCursor extends CursorWrapper { } if (checkItemPlacement(info, dataModel.isFirstPagePinnedItemEnabled)) { dataModel.addItem(mContext, info, false, logger); + if (mRestoreEventLogger != null) { + mRestoreEventLogger.logSingleFavoritesItemRestored(itemType); + } } else { - markDeleted("Item position overlap"); + markDeleted("Item position overlap", RestoreError.INVALID_LOCATION); } } diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index a98ec6484a..17cef900ec 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -17,6 +17,8 @@ package com.android.launcher3.model; import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; +import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed; +import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; @@ -28,16 +30,11 @@ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; -import static com.android.launcher3.util.PackageManagerHelper.isSystemApp; -import android.annotation.SuppressLint; import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -47,12 +44,10 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; -import android.graphics.Point; import android.os.Bundle; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; @@ -60,15 +55,15 @@ import android.util.LongSparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderGridOrganizer; @@ -85,13 +80,11 @@ import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.qsb.QsbContainerView; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult; @@ -103,9 +96,7 @@ import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.WidgetManagerHelper; -import com.android.launcher3.widget.custom.CustomWidgetManager; +import com.android.launcher3.widget.WidgetInflater; import java.util.ArrayList; import java.util.Collections; @@ -113,6 +104,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CancellationException; @@ -123,6 +115,7 @@ import java.util.concurrent.CancellationException; * - all apps icons * - deep shortcuts within apps */ +@SuppressWarnings("NewApi") public class LoaderTask implements Runnable { private static final String TAG = "LoaderTask"; public static final String SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen"; @@ -134,6 +127,7 @@ public class LoaderTask implements Runnable { private final AllAppsList mBgAllAppsList; protected final BgDataModel mBgDataModel; private final ModelDelegate mModelDelegate; + private boolean mIsRestoreFromBackup; private FirstScreenBroadcast mFirstScreenBroadcast; @@ -148,9 +142,9 @@ public class LoaderTask implements Runnable { private final IconCache mIconCache; private final UserManagerState mUserManagerState; - protected final Map mWidgetProvidersMap = new ArrayMap<>(); private Map mShortcutKeyToPinnedShortcuts; + private HashMap mInstallingPkgsCached; private boolean mStopped; @@ -172,13 +166,13 @@ public class LoaderTask implements Runnable { mBgDataModel = bgModel; mModelDelegate = modelDelegate; mLauncherBinder = launcherBinder; - mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class); mUserManager = mApp.getContext().getSystemService(UserManager.class); - mUserCache = UserCache.getInstance(mApp.getContext()); + mUserCache = UserCache.INSTANCE.get(mApp.getContext()); mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext()); mIconCache = mApp.getIconCache(); mUserManagerState = userManagerState; + mInstallingPkgsCached = null; } protected synchronized void waitForIdle() { @@ -221,15 +215,23 @@ public class LoaderTask implements Runnable { TraceHelper.INSTANCE.beginSection(TAG); LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger(); + mIsRestoreFromBackup = + (Boolean) LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE); + LauncherRestoreEventLogger restoreEventLogger = null; + if (enableLauncherBrMetricsFixed()) { + restoreEventLogger = LauncherRestoreEventLogger.Companion + .newInstance(mApp.getContext()); + } try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) { + List allShortcuts = new ArrayList<>(); - loadWorkspace(allShortcuts, "", memoryLogger); + loadWorkspace(allShortcuts, "", memoryLogger, restoreEventLogger); // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db. // sanitizeData should not be invoked if the workspace is loaded from a db different // from the main db as defined in the invariant device profile. // (e.g. both grid preview and minimal device mode uses a different db) - if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) { + if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) { verifyNotStopped(); sanitizeFolders(mItemsDeleted); sanitizeWidgetsShortcutsAndPackages(); @@ -254,7 +256,7 @@ public class LoaderTask implements Runnable { Trace.beginSection("LoadAllApps"); List allActivityList; try { - allActivityList = loadAllApps(); + allActivityList = loadAllApps(); } finally { Trace.endSection(); } @@ -314,8 +316,8 @@ public class LoaderTask implements Runnable { mLauncherBinder.bindWidgets(); logASplit("bindWidgets"); verifyNotStopped(); - LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext()); + if (SMARTSPACE_AS_A_WIDGET.get() && prefs.get(SHOULD_SHOW_SMARTSPACE)) { mLauncherBinder.bindSmartspaceWidget(); // Turn off pref. @@ -349,6 +351,13 @@ public class LoaderTask implements Runnable { mModelDelegate.modelLoadComplete(); transaction.commit(); memoryLogger.clearLogs(); + if (mIsRestoreFromBackup) { + mIsRestoreFromBackup = false; + LauncherPrefs.get(mApp.getContext()).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(false)); + if (restoreEventLogger != null) { + restoreEventLogger.reportLauncherRestoreResults(); + } + } } catch (CancellationException e) { // Loader stopped, ignore logASplit("Cancelled"); @@ -367,10 +376,12 @@ public class LoaderTask implements Runnable { protected void loadWorkspace( List allDeepShortcuts, String selection, - LoaderMemoryLogger memoryLogger) { + LoaderMemoryLogger memoryLogger, + @Nullable LauncherRestoreEventLogger restoreEventLogger + ) { Trace.beginSection("LoadWorkspace"); try { - loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger); + loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger, restoreEventLogger); } finally { Trace.endSection(); } @@ -391,15 +402,15 @@ public class LoaderTask implements Runnable { private void loadWorkspaceImpl( List allDeepShortcuts, String selection, - @Nullable LoaderMemoryLogger memoryLogger) { + @Nullable LoaderMemoryLogger memoryLogger, + @Nullable LauncherRestoreEventLogger restoreEventLogger) { final Context context = mApp.getContext(); final PackageManagerHelper pmHelper = new PackageManagerHelper(context); - final boolean isSafeMode = pmHelper.isSafeMode(); final boolean isSdCardReady = Utilities.isBootCompleted(); - final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context); + final WidgetInflater widgetInflater = new WidgetInflater(context); ModelDbController dbController = mApp.getModel().getModelDbController(); - dbController.tryMigrateDB(); + dbController.tryMigrateDB(restoreEventLogger); Log.d(TAG, "loadWorkspace: loading default favorites"); dbController.loadDefaultFavoritesIfNecessary(); @@ -409,58 +420,36 @@ public class LoaderTask implements Runnable { final HashMap installingPkgs = mSessionHelper.getActiveSessions(); + if (Utilities.enableSupportForArchiving()) { + mInstallingPkgsCached = installingPkgs; + } installingPkgs.forEach(mApp.getIconCache()::updateSessionCache); FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: " + installingPkgs.keySet().stream().map(info -> info.mPackageName).toList()); - final PackageUserKey tempPackageKey = new PackageUserKey(null, null); mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs); mShortcutKeyToPinnedShortcuts = new HashMap<>(); final LoaderCursor c = new LoaderCursor( dbController.query(TABLE_NAME, null, selection, null, null), - mApp, mUserManagerState); + mApp, mUserManagerState, mIsRestoreFromBackup ? restoreEventLogger : null); final Bundle extras = c.getExtras(); mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME); try { final LongSparseArray unlockedUsers = new LongSparseArray<>(); - - mUserManagerState.init(mUserCache, mUserManager); - - for (UserHandle user : mUserCache.getUserProfiles()) { - long serialNo = mUserCache.getSerialNumberForUser(user); - boolean userUnlocked = mUserManager.isUserUnlocked(user); - - // We can only query for shortcuts when the user is unlocked. - if (userUnlocked) { - QueryResult pinnedShortcuts = new ShortcutRequest(context, user) - .query(ShortcutRequest.PINNED); - if (pinnedShortcuts.wasSuccess()) { - for (ShortcutInfo shortcut : pinnedShortcuts) { - mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), - shortcut); - } - if (pinnedShortcuts.isEmpty()) { - FileLog.d(TAG, "No pinned shortcuts found for user " + user); - } - } else { - // Shortcut manager can fail due to some race condition when the - // lock state changes too frequently. For the purpose of the loading - // shortcuts, consider the user is still locked. - FileLog.d(TAG, "Shortcut request failed for user " - + user + ", user may still be locked."); - userUnlocked = false; - } - } - unlockedUsers.put(serialNo, userUnlocked); - } + queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers); List> iconRequestInfos = new ArrayList<>(); + WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger, + mUserManagerState, mLauncherApps, mPendingPackages, + mShortcutKeyToPinnedShortcuts, mApp, mBgDataModel, + mWidgetProvidersMap, installingPkgs, isSdCardReady, + widgetInflater, pmHelper, iconRequestInfos, unlockedUsers, + allDeepShortcuts); + while (!mStopped && c.moveToNext()) { - processWorkspaceItem(c, memoryLogger, installingPkgs, isSdCardReady, - tempPackageKey, widgetHelper, pmHelper, - iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts); + itemProcessor.processItem(); } tryLoadWorkspaceIconsInBulk(iconRequestInfos); } finally { @@ -485,434 +474,97 @@ public class LoaderTask implements Runnable { // Remove dead items mItemsDeleted = c.commitDeleted(); - // Sort the folder items, update ranks, and make sure all preview items are high res. - List verifiers = - mApp.getInvariantDeviceProfile().supportedProfiles.stream().map( - FolderGridOrganizer::new).toList(); - for (FolderInfo folder : mBgDataModel.folders) { - Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); - verifiers.forEach(verifier -> verifier.setFolderInfo(folder)); - int size = folder.contents.size(); - - // Update ranks here to ensure there are no gaps caused by removed folder items. - // Ranks are the source of truth for folder items, so cellX and cellY can be - // ignored for now. Database will be updated once user manually modifies folder. - for (int rank = 0; rank < size; ++rank) { - WorkspaceItemInfo info = folder.contents.get(rank); - // rank is used differently in app pairs, so don't reset - if (folder.itemType != ITEM_TYPE_APP_PAIR) { - info.rank = rank; - } - - if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION - && verifiers.stream().anyMatch( - verifier -> verifier.isItemInPreview(info.rank))) { - mIconCache.getTitleAndIcon(info, false); - } - } - } + processFolderItems(); + processAppPairItems(); c.commitRestoredItems(); } } - private void processWorkspaceItem(LoaderCursor c, - LoaderMemoryLogger memoryLogger, - HashMap installingPkgs, - boolean isSdCardReady, - PackageUserKey tempPackageKey, - WidgetManagerHelper widgetHelper, - PackageManagerHelper pmHelper, - List> iconRequestInfos, - LongSparseArray unlockedUsers, - boolean isSafeMode, - List allDeepShortcuts) { + /** + * After all items have been processed and added to the BgDataModel, this method requests + * high-res icons for the items that are part of an app pair + */ + private void processAppPairItems() { + mBgDataModel.workspaceItems.stream() + .filter((itemInfo -> itemInfo.itemType == ITEM_TYPE_APP_PAIR)) + .forEach(fi -> ((FolderInfo) fi).contents.forEach(item -> + mIconCache.getTitleAndIcon(item, false /*useLowResIcon*/))); + } - try { - if (c.user == null) { - // User has been deleted, remove the item. - c.markDeleted("User has been deleted"); - return; + /** + * Initialized the UserManagerState, and determines which users are unlocked. Additionally, if + * the user is unlocked, it queries LauncherAppsService for pinned shortcuts and stores the + * result in a class variable to be used in other methods while processing workspace items. + * + * @param context used to query LauncherAppsService + * @param unlockedUsers this param is changed, and the updated value is used outside this method + */ + @WorkerThread + private void queryPinnedShortcutsForUnlockedUsers(Context context, + LongSparseArray unlockedUsers) { + mUserManagerState.init(mUserCache, mUserManager); + + for (UserHandle user : mUserCache.getUserProfiles()) { + long serialNo = mUserCache.getSerialNumberForUser(user); + boolean userUnlocked = mUserManager.isUserUnlocked(user); + + // We can only query for shortcuts when the user is unlocked. + if (userUnlocked) { + QueryResult pinnedShortcuts = new ShortcutRequest(context, user) + .query(ShortcutRequest.PINNED); + if (pinnedShortcuts.wasSuccess()) { + for (ShortcutInfo shortcut : pinnedShortcuts) { + mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), + shortcut); + } + if (pinnedShortcuts.isEmpty()) { + FileLog.d(TAG, "No pinned shortcuts found for user " + user); + } + } else { + // Shortcut manager can fail due to some race condition when the + // lock state changes too frequently. For the purpose of the loading + // shortcuts, consider the user is still locked. + FileLog.d(TAG, "Shortcut request failed for user " + + user + ", user may still be locked."); + userUnlocked = false; + } } + unlockedUsers.put(serialNo, userUnlocked); + } - boolean allowMissingTarget = false; - switch (c.itemType) { - case Favorites.ITEM_TYPE_APPLICATION: - case Favorites.ITEM_TYPE_DEEP_SHORTCUT: - Intent intent = c.parseIntent(); - if (intent == null) { - c.markDeleted("Invalid or null intent"); - return; - } + } - int disabledState = mUserManagerState.isUserQuiet(c.serialNumber) - ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0; - ComponentName cn = intent.getComponent(); - String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); + /** + * After all items have been processed and added to the BgDataModel, this method can correctly + * rank items inside folders and load the correct miniature preview icons to be shown when the + * folder is collapsed. + */ + @WorkerThread + private void processFolderItems() { + // Sort the folder items, update ranks, and make sure all preview items are high res. + List verifiers = mApp.getInvariantDeviceProfile().supportedProfiles + .stream().map(FolderGridOrganizer::new).toList(); + for (FolderInfo folder : mBgDataModel.folders) { + Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); + verifiers.forEach(verifier -> verifier.setFolderInfo(folder)); + int size = folder.contents.size(); - if (TextUtils.isEmpty(targetPkg)) { - c.markDeleted("Shortcuts can't have null package"); - return; - } + // Update ranks here to ensure there are no gaps caused by removed folder items. + // Ranks are the source of truth for folder items, so cellX and cellY can be + // ignored for now. Database will be updated once user manually modifies folder. + for (int rank = 0; rank < size; ++rank) { + WorkspaceItemInfo info = folder.contents.get(rank); + // rank is used differently in app pairs, so don't reset + if (folder.itemType != ITEM_TYPE_APP_PAIR) { + info.rank = rank; + } - // If there is no target package, it's an implicit intent - // (legacy shortcut) which is always valid - boolean validTarget = TextUtils.isEmpty(targetPkg) - || mLauncherApps.isPackageEnabled(targetPkg, c.user); - - // If it's a deep shortcut, we'll use pinned shortcuts to restore it - if (cn != null && validTarget && c.itemType - != Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - // If the apk is present and the shortcut points to a specific component. - - // If the component is already present - if (mLauncherApps.isActivityEnabled(cn, c.user)) { - // no special handling necessary for this item - c.markRestored(); - } else { - // Gracefully try to find a fallback activity. - intent = pmHelper.getAppLaunchIntent(targetPkg, c.user); - if (intent != null) { - c.restoreFlag = 0; - c.updater().put( - Favorites.INTENT, - intent.toUri(0)).commit(); - cn = intent.getComponent(); - } else { - c.markDeleted("Unable to find a launch target"); - return; - } - } - } - // else if cn == null => can't infer much, leave it - // else if !validPkg => could be restored icon or missing sd-card - - if (!TextUtils.isEmpty(targetPkg) && !validTarget) { - // Points to a valid app (superset of cn != null) but the apk - // is not available. - - if (c.restoreFlag != 0) { - // Package is not yet available but might be - // installed later. - FileLog.d(TAG, "package not yet restored: " + targetPkg); - tempPackageKey.update(targetPkg, c.user); - if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installingPkgs.containsKey(tempPackageKey)) { - // App restore has started. Update the flag - c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED; - FileLog.d(TAG, "restore started for installing app: " + targetPkg); - c.updater().put(Favorites.RESTORED, c.restoreFlag).commit(); - } else { - c.markDeleted("removing app that is not restored and not " - + "installing. package: " + targetPkg); - return; - } - } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { - // Package is present but not available. - disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE; - // Add the icon on the workspace anyway. - allowMissingTarget = true; - } else if (!isSdCardReady) { - // SdCard is not ready yet. Package might get available, - // once it is ready. - Log.d(TAG, "Missing package, will check later: " + targetPkg); - mPendingPackages.add(new PackageUserKey(targetPkg, c.user)); - // Add the icon on the workspace anyway. - allowMissingTarget = true; - } else { - // Do not wait for external media load anymore. - c.markDeleted("Invalid package removed: " + targetPkg); - return; - } - } - - if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) { - validTarget = false; - } - - if (validTarget) { - // The shortcut points to a valid target (either no target - // or something which is ready to be used) - c.markRestored(); - } - - boolean useLowResIcon = !c.isOnWorkspaceOrHotseat(); - - WorkspaceItemInfo info; - if (c.restoreFlag != 0) { - // Already verified above that user is same as default user - info = c.getRestoredItemInfo(intent); - } else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) { - info = c.getAppShortcutInfo( - intent, allowMissingTarget, useLowResIcon, false); - } else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - ShortcutKey key = ShortcutKey.fromIntent(intent, c.user); - if (unlockedUsers.get(c.serialNumber)) { - ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key); - if (pinnedShortcut == null) { - // The shortcut is no longer valid. - c.markDeleted("Pinned shortcut not found for package: " - + key.getPackageName()); - return; - } - info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext()); - // If the pinned deep shortcut is no longer published, - // use the last saved icon instead of the default. - mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon); - - if (pmHelper.isAppSuspended( - pinnedShortcut.getPackage(), info.user)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; - } - intent = info.getIntent(); - allDeepShortcuts.add(pinnedShortcut); - } else { - // Create a shortcut info in disabled mode for now. - info = c.loadSimpleWorkspaceItem(); - info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; - } - } else { // item type == ITEM_TYPE_SHORTCUT - info = c.loadSimpleWorkspaceItem(); - - // Shortcuts are only available on the primary profile - if (!TextUtils.isEmpty(targetPkg) - && pmHelper.isAppSuspended(targetPkg, c.user)) { - disabledState |= FLAG_DISABLED_SUSPENDED; - } - info.options = c.getOptions(); - - // App shortcuts that used to be automatically added to Launcher - // didn't always have the correct intent flags set, so do that here - if (intent.getAction() != null - && intent.getCategories() != null - && intent.getAction().equals(Intent.ACTION_MAIN) - && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - } - } - - if (info != null) { - if (info.itemType != 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); - - info.intent = intent; - info.rank = c.getRank(); - info.spanX = 1; - info.spanY = 1; - info.runtimeStatusFlags |= disabledState; - if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE; - } - LauncherActivityInfo activityInfo = c.getLauncherActivityInfo(); - if (activityInfo != null) { - info.setProgressLevel( - PackageManagerHelper.getLoadingProgress(activityInfo), - PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); - } - - if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) { - tempPackageKey.update(targetPkg, c.user); - SessionInfo si = installingPkgs.get(tempPackageKey); - if (si == null) { - info.runtimeStatusFlags - &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; - } else if (activityInfo == null) { - int installProgress = (int) (si.getProgress() * 100); - - info.setProgressLevel(installProgress, - PackageInstallInfo.STATUS_INSTALLING); - } - } - - c.checkAndAddItem(info, mBgDataModel, memoryLogger); - } else { - throw new RuntimeException("Unexpected null WorkspaceItemInfo"); - } - break; - - case Favorites.ITEM_TYPE_FOLDER: - case Favorites.ITEM_TYPE_APP_PAIR: - FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id); - c.applyCommonProperties(folderInfo); - - folderInfo.itemType = c.itemType; - // Do not trim the folder label, as is was set by the user. - folderInfo.title = c.getString(c.mTitleIndex); - folderInfo.spanX = 1; - folderInfo.spanY = 1; - folderInfo.options = c.getOptions(); - - // no special handling required for restored folders - c.markRestored(); - - c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger); - break; - - case Favorites.ITEM_TYPE_APPWIDGET: - if (WidgetsModel.GO_DISABLE_WIDGETS) { - c.markDeleted("Only legacy shortcuts can have null package"); - return; - } - // Follow through - case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: - // Read all Launcher-specific widget details - boolean customWidget = c.itemType - == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - - int appWidgetId = c.getAppWidgetId(); - String savedProvider = c.getAppWidgetProvider(); - final ComponentName component; - - if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) { - component = QsbContainerView.getSearchComponentName(mApp.getContext()); - if (component == null) { - c.markDeleted("Discarding SearchWidget without packagename "); - return; - } - } else { - component = ComponentName.unflattenFromString(savedProvider); - } - final boolean isIdValid = - !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); - final boolean wasProviderReady = - !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY); - - ComponentKey providerKey = new ComponentKey(component, c.user); - if (!mWidgetProvidersMap.containsKey(providerKey)) { - if (customWidget) { - mWidgetProvidersMap.put(providerKey, CustomWidgetManager.INSTANCE - .get(mApp.getContext()).getWidgetProvider(component)); - } else { - mWidgetProvidersMap.put(providerKey, - widgetHelper.findProvider(component, c.user)); - } - } - final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey); - - final boolean isProviderReady = isValidProvider(provider); - if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) { - c.markDeleted("Deleting widget that isn't installed anymore: " + provider); - } else { - LauncherAppWidgetInfo appWidgetInfo; - if (isProviderReady) { - appWidgetInfo = - new LauncherAppWidgetInfo(appWidgetId, provider.provider); - - // The provider is available. So the widget is either - // available or not available. We do not need to track - // any future restore updates. - int status = c.restoreFlag - & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED - & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; - if (!wasProviderReady) { - // If provider was not previously ready, update status and UI flag. - - // Id would be valid only if the widget restore broadcast received. - if (isIdValid) { - status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - } - } - appWidgetInfo.restoreStatus = status; - } else { - Log.v(TAG, "Widget restore pending id=" + c.id - + " appWidgetId=" + appWidgetId - + " status=" + c.restoreFlag); - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component); - appWidgetInfo.restoreStatus = c.restoreFlag; - - tempPackageKey.update(component.getPackageName(), c.user); - SessionInfo si = installingPkgs.get(tempPackageKey); - Integer installProgress = si == null - ? null - : (int) (si.getProgress() * 100); - - if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installProgress != null) { - // App restore has started. Update the flag - appWidgetInfo.restoreStatus - |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; - } else if (!isSafeMode) { - c.markDeleted("Unrestored widget removed: " + component); - return; - } - - appWidgetInfo.installProgress = - installProgress == null ? 0 : installProgress; - } - if (appWidgetInfo.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { - appWidgetInfo.bindOptions = c.parseIntent(); - } - - c.applyCommonProperties(appWidgetInfo); - appWidgetInfo.spanX = c.getSpanX(); - appWidgetInfo.spanY = c.getSpanY(); - appWidgetInfo.options = c.getOptions(); - appWidgetInfo.user = c.user; - appWidgetInfo.sourceContainer = c.getAppWidgetSource(); - - if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { - c.markDeleted("Widget has invalid size: " - + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY); - return; - } - LauncherAppWidgetProviderInfo widgetProviderInfo = - widgetHelper.getLauncherAppWidgetInfo(appWidgetId, - appWidgetInfo.getTargetComponent()); - if (widgetProviderInfo != null - && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX - || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) { - FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent() - + " minSizes not meet: span=" + appWidgetInfo.spanX - + "x" + appWidgetInfo.spanY + " minSpan=" - + widgetProviderInfo.minSpanX + "x" - + widgetProviderInfo.minSpanY); - logWidgetInfo(mApp.getInvariantDeviceProfile(), - widgetProviderInfo); - } - if (!c.isOnWorkspaceOrHotseat()) { - c.markDeleted("Widget found where container != CONTAINER_DESKTOP" - + "nor CONTAINER_HOTSEAT - ignoring!"); - return; - } - - if (!customWidget) { - String providerName = appWidgetInfo.providerName.flattenToString(); - if (!providerName.equals(savedProvider) - || (appWidgetInfo.restoreStatus != c.restoreFlag)) { - c.updater() - .put(Favorites.APPWIDGET_PROVIDER, - providerName) - .put(Favorites.RESTORED, - appWidgetInfo.restoreStatus) - .commit(); - } - } - - if (appWidgetInfo.restoreStatus - != LauncherAppWidgetInfo.RESTORE_COMPLETED) { - appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo( - mApp.getContext(), - appWidgetInfo.providerName, - appWidgetInfo.user); - mIconCache.getTitleAndIconForApp( - appWidgetInfo.pendingItemInfo, false); - } - - c.checkAndAddItem(appWidgetInfo, mBgDataModel); - } - break; + if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION + && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) { + mIconCache.getTitleAndIcon(info, false); + } } - } catch (Exception e) { - Log.e(TAG, "Desktop items loading interrupted", e); } } @@ -1014,7 +666,21 @@ public class LoaderTask implements Runnable { // Create the ApplicationInfos for (int i = 0; i < apps.size(); i++) { LauncherActivityInfo app = apps.get(i); - AppInfo appInfo = new AppInfo(app, user, quietMode); + AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user), quietMode); + if (Utilities.enableSupportForArchiving() && app.getApplicationInfo().isArchived) { + // For archived apps, include progress info in case there is a pending + // install session post restart of device. + String appPackageName = app.getApplicationInfo().packageName; + SessionInfo si = mInstallingPkgsCached != null ? mInstallingPkgsCached.get( + new PackageUserKey(appPackageName, user)) + : mSessionHelper.getActiveSessionInfo(user, + appPackageName); + if (si != null) { + appInfo.runtimeStatusFlags |= FLAG_INSTALL_SESSION_ACTIVE; + appInfo.setProgressLevel((int) (si.getProgress() * 100), + PackageInstallInfo.STATUS_INSTALLING); + } + } iconRequestInfos.add(new IconRequestInfo<>( appInfo, app, /* useLowResIcon= */ false)); @@ -1108,52 +774,6 @@ public class LoaderTask implements Runnable { && (provider.provider.getPackageName() != null); } - @SuppressLint("NewApi") // Already added API check. - private static void logWidgetInfo(InvariantDeviceProfile idp, - LauncherAppWidgetProviderInfo widgetProviderInfo) { - Point cellSize = new Point(); - for (DeviceProfile deviceProfile : idp.supportedProfiles) { - deviceProfile.getCellSize(cellSize); - FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx - + ", available height: " + deviceProfile.availableHeightPx - + ", cellLayoutBorderSpacePx Horizontal: " - + deviceProfile.cellLayoutBorderSpacePx.x - + ", cellLayoutBorderSpacePx Vertical: " - + deviceProfile.cellLayoutBorderSpacePx.y - + ", cellSize: " + cellSize); - } - - StringBuilder widgetDimension = new StringBuilder(); - widgetDimension.append("Widget dimensions:\n") - .append("minResizeWidth: ") - .append(widgetProviderInfo.minResizeWidth) - .append("\n") - .append("minResizeHeight: ") - .append(widgetProviderInfo.minResizeHeight) - .append("\n") - .append("defaultWidth: ") - .append(widgetProviderInfo.minWidth) - .append("\n") - .append("defaultHeight: ") - .append(widgetProviderInfo.minHeight) - .append("\n"); - if (Utilities.ATLEAST_S) { - widgetDimension.append("targetCellWidth: ") - .append(widgetProviderInfo.targetCellWidth) - .append("\n") - .append("targetCellHeight: ") - .append(widgetProviderInfo.targetCellHeight) - .append("\n") - .append("maxResizeWidth: ") - .append(widgetProviderInfo.maxResizeWidth) - .append("\n") - .append("maxResizeHeight: ") - .append(widgetProviderInfo.maxResizeHeight) - .append("\n"); - } - FileLog.d(TAG, widgetDimension.toString()); - } - private static void logASplit(String label) { if (DEBUG) { Log.d(TAG, label); diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java index d2b7161697..ba2b64d9c1 100644 --- a/src/com/android/launcher3/model/ModelDbController.java +++ b/src/com/android/launcher3/model/ModelDbController.java @@ -19,6 +19,7 @@ import static android.util.Base64.NO_PADDING; import static android.util.Base64.NO_WRAP; import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE; import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb; import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY; import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL; @@ -48,6 +49,7 @@ import android.util.Base64; import android.util.Log; import android.util.Xml; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.AutoInstallsLayout; @@ -62,6 +64,8 @@ import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError; import com.android.launcher3.logging.FileLog; import com.android.launcher3.pm.UserCache; import com.android.launcher3.provider.LauncherDbUtils; @@ -261,8 +265,12 @@ public class ModelDbController { /** * Migrates the DB if needed. If the migration failed, it clears the DB. */ - public void tryMigrateDB() { + public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) { + if (!migrateGridIfNeeded()) { + if (restoreEventLogger != null) { + sendMetricsForFailedMigration(restoreEventLogger, getDb()); + } FileLog.d(TAG, "Migration failed: resetting launcher database"); createEmptyDB(); LauncherPrefs.get(mContext).putSync( @@ -312,6 +320,30 @@ public class ModelDbController { } } + /** + * In case of migration failure, report metrics for the count of each itemType in the DB. + * @param restoreEventLogger logger used to report Launcher restore metrics + */ + private void sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger, + SQLiteDatabase db) { + try (Cursor cursor = db.rawQuery( + "SELECT itemType, COUNT(*) AS count FROM favorites GROUP BY itemType", + null + )) { + if (cursor.moveToFirst()) { + do { + restoreEventLogger.logFavoritesItemsRestoreFailed( + cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow("count")), + RestoreError.GRID_MIGRATION_FAILURE + ); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + FileLog.e(TAG, "sendMetricsForFailedDb: Error reading from database", e); + } + } + /** * Returns the underlying model database */ @@ -426,7 +458,7 @@ public class ModelDbController { LauncherWidgetHolder widgetHolder) { ContentResolver cr = mContext.getContentResolver(); String blobHandlerDigest = Settings.Secure.getString(cr, LAYOUT_DIGEST_KEY); - if (Utilities.ATLEAST_R && !TextUtils.isEmpty(blobHandlerDigest)) { + if (!TextUtils.isEmpty(blobHandlerDigest)) { BlobStoreManager blobManager = mContext.getSystemService(BlobStoreManager.class); try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream( blobManager.openBlob(BlobHandle.createWithSha256( diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt new file mode 100644 index 0000000000..b12b2bc9d2 --- /dev/null +++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 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.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.os.UserHandle +import android.text.TextUtils +import com.android.launcher3.LauncherModel.ModelUpdateTask +import com.android.launcher3.logging.FileLog +import com.android.launcher3.model.PackageUpdatedTask.OP_ADD +import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE +import com.android.launcher3.model.PackageUpdatedTask.OP_SUSPEND +import com.android.launcher3.model.PackageUpdatedTask.OP_UNAVAILABLE +import com.android.launcher3.model.PackageUpdatedTask.OP_UNSUSPEND +import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE +import java.util.function.Consumer + +/** + * Implementation of {@link LauncherApps#Callbacks} which converts various events to corresponding + * model tasks + */ +class ModelLauncherCallbacks(private var taskExecutor: Consumer) : + LauncherApps.Callback() { + + override fun onPackageAdded(packageName: String, user: UserHandle) { + taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName)) + } + + override fun onPackageChanged(packageName: String, user: UserHandle) { + taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, packageName)) + } + + override fun onPackageLoadingProgressChanged( + packageName: String, + user: UserHandle, + progress: Float + ) { + taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress)) + } + + override fun onPackageRemoved(packageName: String, user: UserHandle) { + FileLog.d(TAG, "package removed received $packageName") + taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName)) + } + + override fun onPackagesAvailable( + vararg packageNames: String, + user: UserHandle, + replacing: Boolean + ) { + taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames)) + } + + override fun onPackagesSuspended(vararg packageNames: String, user: UserHandle) { + taskExecutor.accept(PackageUpdatedTask(OP_SUSPEND, user, *packageNames)) + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + if (!replacing) { + taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames)) + } + } + + override fun onPackagesUnsuspended(vararg packageNames: String, user: UserHandle) { + taskExecutor.accept(PackageUpdatedTask(OP_UNSUSPEND, user, *packageNames)) + } + + override fun onShortcutsChanged( + packageName: String, + shortcuts: MutableList, + user: UserHandle + ) { + taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true)) + } + + fun onPackagesRemoved(user: UserHandle, packages: List) { + FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages)) + taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, *packages.toTypedArray())) + } + + companion object { + private const val TAG = "LauncherAppsCallbackImpl" + } +} diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java index bc51c9bfad..9e72e2823e 100644 --- a/src/com/android/launcher3/model/ModelUtils.java +++ b/src/com/android/launcher3/model/ModelUtils.java @@ -20,7 +20,6 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -37,9 +36,9 @@ public class ModelUtils { */ public static void filterCurrentWorkspaceItems( final IntSet currentScreenIds, - ArrayList allWorkspaceItems, - ArrayList currentScreenItems, - ArrayList otherScreenItems) { + List allWorkspaceItems, + List currentScreenItems, + List otherScreenItems) { // Purge any null ItemInfos allWorkspaceItems.removeIf(Objects::isNull); // Order the set of items by their containers first, this allows use to walk through the diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java index 76a87ed1da..2457a42d6a 100644 --- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java +++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java @@ -52,7 +52,8 @@ public class PackageInstallStateChangedTask extends BaseModelUpdateTask { ApplicationInfo ai = app.getContext() .getPackageManager().getApplicationInfo(mInstallInfo.packageName, 0); if (InstantAppResolver.newInstance(app.getContext()).isInstantApp(ai)) { - app.getModel().onPackageAdded(ai.packageName, mInstallInfo.user); + app.getModel().newModelCallbacks() + .onPackageAdded(ai.packageName, mInstallInfo.user); } } catch (PackageManager.NameNotFoundException e) { // Ignore diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 4f2d398b42..0ba468d5f0 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -18,6 +18,7 @@ package com.android.launcher3.model; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON; @@ -37,6 +38,7 @@ import com.android.launcher3.Flags; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.FileLog; @@ -46,6 +48,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutRequest; +import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.FlagOp; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; @@ -67,6 +70,7 @@ import java.util.stream.Collectors; * Handles updates due to changes in package manager (app installed/updated/removed) * or when a user availability changes. */ +@SuppressWarnings("NewApi") public class PackageUpdatedTask extends BaseModelUpdateTask { // TODO(b/290090023): Set to false after root causing is done. @@ -237,8 +241,9 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { isTargetValid = context.getSystemService(LauncherApps.class) .isActivityEnabled(cn, mUser); } - if (!isTargetValid && si.hasStatusFlag( - FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) { + if (!isTargetValid && (si.hasStatusFlag( + FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON) + || si.isArchived())) { if (updateWorkspaceItemIntent(context, si, packageName)) { infoUpdated = true; } else if (si.hasPromiseIconUi()) { @@ -269,7 +274,23 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { : PackageManagerHelper.getLoadingProgress( activities.get(0)), PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); + // In case an app is archived, we need to make sure that archived state + // in WorkspaceItemInfo is refreshed. + if (Utilities.enableSupportForArchiving() && !activities.isEmpty()) { + boolean newArchivalState = activities.get( + 0).getActivityInfo().isArchived; + if (newArchivalState != si.isArchived()) { + si.runtimeStatusFlags ^= FLAG_ARCHIVED; + infoUpdated = true; + } + } if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) { + if (activities != null && !activities.isEmpty()) { + si.status = ApiWrapper + .isNonResizeableActivity(activities.get(0)) + ? si.status | WorkspaceItemInfo.FLAG_NON_RESIZEABLE + : si.status & ~WorkspaceItemInfo.FLAG_NON_RESIZEABLE; + } iconCache.getTitleAndIcon(si, si.usingLowResIcon()); infoUpdated = true; } @@ -340,9 +361,17 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { - Predicate removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) - .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) - .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); + // This predicate is used to mark an ItemInfo for removal if its package or component + // is marked for removal. + Predicate removeAppMatch = + ItemInfoMatcher.ofPackages(removedPackages, mUser) + .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) + .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); + // This predicate is used to mark an app pair for removal if it contains an app marked + // for removal. + Predicate removeAppPairMatch = + ItemInfoMatcher.forAppPairMatch(removeAppMatch); + Predicate removeMatch = removeAppMatch.or(removeAppPairMatch); deleteAndBindComponentsRemoved(removeMatch, "removed because the corresponding package or component is removed. " + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect( diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java index 3798575f8a..8cfa3aa7d9 100644 --- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java +++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java @@ -67,11 +67,10 @@ public class SdCardAvailableReceiver extends BroadcastReceiver { } } if (!packagesRemoved.isEmpty()) { - mModel.onPackagesRemoved(user, - packagesRemoved.toArray(new String[packagesRemoved.size()])); + mModel.newModelCallbacks().onPackagesRemoved(user, packagesRemoved); } if (!packagesUnavailable.isEmpty()) { - mModel.onPackagesUnavailable( + mModel.newModelCallbacks().onPackagesUnavailable( packagesUnavailable.toArray(new String[packagesUnavailable.size()]), user, false); } diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java index c99b8891c8..1dd58c3d22 100644 --- a/src/com/android/launcher3/model/WidgetItem.java +++ b/src/com/android/launcher3/model/WidgetItem.java @@ -1,5 +1,9 @@ package com.android.launcher3.model; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX; + import static com.android.launcher3.Utilities.ATLEAST_S; import android.annotation.SuppressLint; @@ -7,13 +11,20 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.util.SparseArray; +import android.widget.RemoteViews; +import androidx.core.os.BuildCompat; + +import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; +import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.IconCache; import com.android.launcher3.pm.ShortcutConfigActivityInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.WidgetManagerHelper; /** * An wrapper over various items displayed in a widget picker, @@ -25,12 +36,15 @@ public class WidgetItem extends ComponentKey { public final LauncherAppWidgetProviderInfo widgetInfo; public final ShortcutConfigActivityInfo activityInfo; + public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO; public final String label; public final CharSequence description; public final int spanX, spanY; + public final SparseArray generatedPreviews; public WidgetItem(LauncherAppWidgetProviderInfo info, - InvariantDeviceProfile idp, IconCache iconCache, Context context) { + InvariantDeviceProfile idp, IconCache iconCache, Context context, + WidgetManagerHelper helper) { super(info.provider, info.getProfile()); label = iconCache.getTitleNoCache(info); @@ -40,6 +54,27 @@ public class WidgetItem extends ComponentKey { spanX = Math.min(info.spanX, idp.numColumns); spanY = Math.min(info.spanY, idp.numRows); + + if (BuildCompat.isAtLeastV() && Flags.enableGeneratedPreviews()) { + generatedPreviews = new SparseArray<>(3); + for (int widgetCategory : new int[] { + WIDGET_CATEGORY_HOME_SCREEN, + WIDGET_CATEGORY_KEYGUARD, + WIDGET_CATEGORY_SEARCHBOX, + }) { + if ((widgetCategory & widgetInfo.generatedPreviewCategories) != 0) { + generatedPreviews.put(widgetCategory, + helper.loadGeneratedPreview(widgetInfo, widgetCategory)); + } + } + } else { + generatedPreviews = null; + } + } + + public WidgetItem(LauncherAppWidgetProviderInfo info, + InvariantDeviceProfile idp, IconCache iconCache, Context context) { + this(info, idp, iconCache, context, new WidgetManagerHelper(context)); } public WidgetItem(ShortcutConfigActivityInfo info, IconCache iconCache, PackageManager pm) { @@ -50,6 +85,7 @@ public class WidgetItem extends ComponentKey { widgetInfo = null; activityInfo = info; spanX = spanY = 1; + generatedPreviews = null; } /** @@ -78,4 +114,15 @@ public class WidgetItem extends ComponentKey { public boolean isShortcut() { return activityInfo != null; } + + /** + * Returns whether this {@link WidgetItem} has a generated preview for the given widget + * category. + */ + public boolean hasGeneratedPreview(int widgetCategory) { + if (!Flags.enableGeneratedPreviews() || generatedPreviews == null) { + return false; + } + return generatedPreviews.contains(widgetCategory); + } } diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt new file mode 100644 index 0000000000..59f56dfaa8 --- /dev/null +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2023 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.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.PackageInstaller +import android.content.pm.ShortcutInfo +import android.graphics.Point +import android.text.TextUtils +import android.util.Log +import android.util.LongSparseArray +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.Utilities +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError +import com.android.launcher3.logging.FileLog +import com.android.launcher3.model.data.IconRequestInfo +import com.android.launcher3.model.data.ItemInfoWithIcon +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.pm.PackageInstallInfo +import com.android.launcher3.shortcuts.ShortcutKey +import com.android.launcher3.uioverrides.ApiWrapper +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.PackageManagerHelper +import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.WidgetInflater +import com.android.launcher3.widget.util.WidgetSizes + +/** + * This items is used by LoaderTask to process items that have been loaded from the Launcher's DB. + * This data, stored in the Favorites table, needs to be processed in order to be shown on the Home + * Page. + * + * This class processes each of those items: App Shortcuts, Widgets, Folders, etc., one at a time. + */ +class WorkspaceItemProcessor( + private val c: LoaderCursor, + private val memoryLogger: LoaderMemoryLogger?, + private val userManagerState: UserManagerState, + private val launcherApps: LauncherApps, + private val pendingPackages: MutableSet, + private val shortcutKeyToPinnedShortcuts: Map, + private val app: LauncherAppState, + private val bgDataModel: BgDataModel, + private val widgetProvidersMap: MutableMap, + private val installingPkgs: HashMap, + private val isSdCardReady: Boolean, + private val widgetInflater: WidgetInflater, + private val pmHelper: PackageManagerHelper, + private val iconRequestInfos: MutableList>, + private val unlockedUsers: LongSparseArray, + private val allDeepShortcuts: MutableList +) { + + private val isSafeMode = app.isSafeModeEnabled + private val tempPackageKey = PackageUserKey(null, null) + private val iconCache = app.iconCache + + /** + * This is the entry point for processing 1 workspace item. This method is like the midfielder + * that delegates the actual processing to either processAppShortcut, processFolder, or + * processWidget depending on what type of item is being processed. + * + * All the parameters are expected to be shared between many repeated calls of this method, one + * for each workspace item. + */ + fun processItem() { + try { + if (c.user == null) { + // User has been deleted, remove the item. + c.markDeleted( + "User has been deleted for item id=${c.id}", + RestoreError.PROFILE_DELETED + ) + return + } + when (c.itemType) { + Favorites.ITEM_TYPE_APPLICATION, + Favorites.ITEM_TYPE_DEEP_SHORTCUT -> processAppOrDeepShortcut() + Favorites.ITEM_TYPE_FOLDER, + Favorites.ITEM_TYPE_APP_PAIR -> processFolderOrAppPair() + Favorites.ITEM_TYPE_APPWIDGET, + Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> processWidget() + } + } catch (e: Exception) { + Log.e(TAG, "Desktop items loading interrupted", e) + } + } + + /** + * This method verifies that an app shortcut should be shown on the home screen, updates the + * database accordingly, formats the data in such a way that it is ready to be added to the data + * model, and then adds it to the launcher’s data model. + * + * In this method, verification means that an an app shortcut database entry is required to: + * Have a Launch Intent. This is how the app component symbolized by the shortcut is launched. + * Have a Package Name. Not be in a funky “Restoring, but never actually restored” state. Not + * have null or missing ShortcutInfos or ItemInfos in other data models. + * + * If any of the above are found to be true, the database entry is deleted, and not shown on the + * user’s home screen. When an app is verified, it is marked as restored, meaning that the app + * is viable to show on the home screen. + * + * In order to accommodate different types and versions of App Shortcuts, different properties + * and flags are set on the ItemInfo objects that are added to the data model. For example, + * icons that are not a part of the workspace or hotseat are marked as using low resolution icon + * bitmaps. Currently suspended app icons are marked as such. Installing packages are also + * marked as such. Lastly, after applying common properties to the ItemInfo, it is added to the + * data model to be bound to the launcher’s data model. + */ + @SuppressLint("NewApi") + private fun processAppOrDeepShortcut() { + var allowMissingTarget = false + var intent = c.parseIntent() + if (intent == null) { + c.markDeleted("Null intent from db for item id=${c.id}", RestoreError.MISSING_INFO) + return + } + var disabledState = + if (userManagerState.isUserQuiet(c.serialNumber)) + WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER + else 0 + val cn = intent.component + val targetPkg = cn?.packageName ?: intent.getPackage() + if (targetPkg.isNullOrEmpty()) { + c.markDeleted("No target package for item id=${c.id}", RestoreError.MISSING_INFO) + return + } + var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user) + + // If it's a deep shortcut, we'll use pinned shortcuts to restore it + if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) { + // If the apk is present and the shortcut points to a specific component. + + // If the component is already present + if (launcherApps.isActivityEnabled(cn, c.user)) { + // no special handling necessary for this item + c.markRestored() + } else { + // Gracefully try to find a fallback activity. + FileLog.d( + TAG, + "Activity not enabled for id=${c.id}, component=$cn, user=${c.user}." + + " Will attempt to find fallback Activity for targetPkg=$targetPkg." + ) + intent = pmHelper.getAppLaunchIntent(targetPkg, c.user) + if (intent != null) { + c.restoreFlag = 0 + c.updater().put(Favorites.INTENT, intent.toUri(0)).commit() + } else { + c.markDeleted( + "No Activities found for id=${c.id}, targetPkg=$targetPkg, component=$cn." + + " Unable to create launch Intent.", + RestoreError.MISSING_INFO + ) + return + } + } + } + // else if cn == null => can't infer much, leave it + // else if !validPkg => could be restored icon or missing sd-card + when { + !TextUtils.isEmpty(targetPkg) && !validTarget -> { + // Points to a valid app (superset of cn != null) but the apk + // is not available. + when { + c.restoreFlag != 0 -> { + // Package is not yet available but might be + // installed later. + FileLog.d(TAG, "package not yet restored: $targetPkg") + tempPackageKey.update(targetPkg, c.user) + when { + c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> { + // Restore has started once. + } + installingPkgs.containsKey(tempPackageKey) -> { + // App restore has started. Update the flag + c.restoreFlag = + c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED + FileLog.d(TAG, "restore started for installing app: $targetPkg") + c.updater().put(Favorites.RESTORED, c.restoreFlag).commit() + } + else -> { + c.markDeleted( + "removing app that is not restored and not installing. package: $targetPkg", + RestoreError.APP_NOT_INSTALLED + ) + return + } + } + } + pmHelper.isAppOnSdcard(targetPkg, c.user) -> { + // Package is present but not available. + disabledState = + disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE + // Add the icon on the workspace anyway. + allowMissingTarget = true + } + !isSdCardReady -> { + // SdCard is not ready yet. Package might get available, + // once it is ready. + Log.d(TAG, "Missing package, will check later: $targetPkg") + pendingPackages.add(PackageUserKey(targetPkg, c.user)) + // Add the icon on the workspace anyway. + allowMissingTarget = true + } + else -> { + // Do not wait for external media load anymore. + c.markDeleted( + "Invalid package removed: $targetPkg", + RestoreError.APP_NOT_INSTALLED + ) + return + } + } + } + } + if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) { + validTarget = false + } + if (validTarget) { + // The shortcut points to a valid target (either no target + // or something which is ready to be used) + c.markRestored() + } + val useLowResIcon = !c.isOnWorkspaceOrHotseat + val info: WorkspaceItemInfo? + when { + c.restoreFlag != 0 -> { + // Already verified above that user is same as default user + info = c.getRestoredItemInfo(intent) + } + c.itemType == Favorites.ITEM_TYPE_APPLICATION -> + info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false) + c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT -> { + val key = ShortcutKey.fromIntent(intent, c.user) + if (unlockedUsers[c.serialNumber]) { + val pinnedShortcut = shortcutKeyToPinnedShortcuts[key] + if (pinnedShortcut == null) { + // The shortcut is no longer valid. + c.markDeleted( + "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}", + RestoreError.SHORTCUT_NOT_FOUND + ) + return + } + info = WorkspaceItemInfo(pinnedShortcut, app.context) + // If the pinned deep shortcut is no longer published, + // use the last saved icon instead of the default. + iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon) + if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) { + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED + } + intent = info.getIntent() + allDeepShortcuts.add(pinnedShortcut) + } else { + // Create a shortcut info in disabled mode for now. + info = c.loadSimpleWorkspaceItem() + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER + } + } + else -> { // item type == ITEM_TYPE_SHORTCUT + info = c.loadSimpleWorkspaceItem() + + // Shortcuts are only available on the primary profile + if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg, c.user)) { + disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED + } + info.options = c.options + + // App shortcuts that used to be automatically added to Launcher + // didn't always have the correct intent flags set, so do that here + if ( + intent.action != null && + intent.categories != null && + intent.action == Intent.ACTION_MAIN && + intent.categories.contains(Intent.CATEGORY_LAUNCHER) + ) { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + } + } + } + if (info != null) { + if (info.itemType != 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) + info.intent = intent + info.rank = c.rank + info.spanX = 1 + info.spanY = 1 + info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState + if (isSafeMode && !PackageManagerHelper.isSystemApp(app.context, intent)) { + info.runtimeStatusFlags = + info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE + } + val activityInfo = c.launcherActivityInfo + if (activityInfo != null) { + if (ApiWrapper.isNonResizeableActivity(activityInfo)) { + info.status = info.status or WorkspaceItemInfo.FLAG_NON_RESIZEABLE + } + info.setProgressLevel( + PackageManagerHelper.getLoadingProgress(activityInfo), + PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING + ) + } + if ( + (c.restoreFlag != 0 || + Utilities.enableSupportForArchiving() && + activityInfo != null && + activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg) + ) { + tempPackageKey.update(targetPkg, c.user) + val si = installingPkgs[tempPackageKey] + if (si == null) { + info.runtimeStatusFlags = + info.runtimeStatusFlags and + ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE.inv() + } else if ( + activityInfo == null || + (Utilities.enableSupportForArchiving() && + activityInfo.applicationInfo.isArchived) + ) { + // For archived apps, include progress info in case there is + // a pending install session post restart of device. + val installProgress = (si.getProgress() * 100).toInt() + info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING) + } + } + c.checkAndAddItem(info, bgDataModel, memoryLogger) + } else { + throw RuntimeException("Unexpected null WorkspaceItemInfo") + } + } + + /** + * Loads the folder information from the database and formats it into a FolderInfo. Some of the + * processing for folder content items is done in LoaderTask after all the items in the + * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel. + */ + private fun processFolderOrAppPair() { + val folderInfo = + bgDataModel.findOrMakeFolder(c.id).apply { + c.applyCommonProperties(this) + itemType = c.itemType + // Do not trim the folder label, as is was set by the user. + title = c.getString(c.mTitleIndex) + spanX = 1 + spanY = 1 + options = c.options + } + + // no special handling required for restored folders + c.markRestored() + c.checkAndAddItem(folderInfo, bgDataModel, memoryLogger) + } + + /** + * This method, similar to processAppShortcut above, verifies that a widget should be shown on + * the home screen, updates the database accordingly, formats the data in such a way that it is + * ready to be added to the data model, and then adds it to the launcher’s data model. + * + * It verifies that: Widgets are not disabled due to the Launcher variety being of the `Go` + * type. Search Widgets have a package name. The app behind the widget is still installed on the + * device. The app behind the widget is not in a funky “Restoring, but never actually restored” + * state. The widget has a valid size. The widget is in the workspace or the hotseat. If any of + * the above are found to be true, the database entry is deleted, and the widget is not shown on + * the user’s home screen. When a widget is verified, it is marked as restored, meaning that the + * widget is viable to show on the home screen. + * + * Common properties are applied to the Widget’s Info object, and other information as well + * depending on the type of widget. Custom widgets are treated differently than non-custom + * widgets, installing / restoring widgets are treated differently, etc. + */ + private fun processWidget() { + val component = ComponentName.unflattenFromString(c.appWidgetProvider)!! + val appWidgetInfo = LauncherAppWidgetInfo(c.appWidgetId, component) + c.applyCommonProperties(appWidgetInfo) + appWidgetInfo.spanX = c.spanX + appWidgetInfo.spanY = c.spanY + appWidgetInfo.options = c.options + appWidgetInfo.user = c.user + appWidgetInfo.sourceContainer = c.appWidgetSource + appWidgetInfo.restoreStatus = c.restoreFlag + if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { + c.markDeleted( + "Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}", + RestoreError.INVALID_LOCATION + ) + return + } + if (!c.isOnWorkspaceOrHotseat) { + c.markDeleted( + "Widget found where container != CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!", + RestoreError.INVALID_LOCATION + ) + return + } + if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { + appWidgetInfo.bindOptions = c.parseIntent() + } + val inflationResult = widgetInflater.inflateAppWidget(appWidgetInfo) + var shouldUpdate = inflationResult.isUpdate + val lapi = inflationResult.widgetInfo + + when (inflationResult.type) { + WidgetInflater.TYPE_DELETE -> { + c.markDeleted(inflationResult.reason, inflationResult.restoreErrorType) + return + } + WidgetInflater.TYPE_PENDING -> { + tempPackageKey.update(component.packageName, c.user) + val si = installingPkgs[tempPackageKey] + + if ( + !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && + !isSafeMode && + (si == null) && + (lapi == null) && + !(Utilities.enableSupportForArchiving() && + pmHelper.isAppArchived(component.packageName)) + ) { + // Restore never started + c.markDeleted( + "Unrestored widget removed: $component", + RestoreError.APP_NOT_INSTALLED + ) + return + } else if ( + !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && si != null + ) { + shouldUpdate = true + appWidgetInfo.restoreStatus = + appWidgetInfo.restoreStatus or LauncherAppWidgetInfo.FLAG_RESTORE_STARTED + } + appWidgetInfo.installProgress = + if (si == null) 0 else (si.getProgress() * 100).toInt() + appWidgetInfo.pendingItemInfo = + WidgetsModel.newPendingItemInfo( + app.context, + appWidgetInfo.providerName, + appWidgetInfo.user + ) + iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false) + } + WidgetInflater.TYPE_REAL -> + WidgetSizes.updateWidgetSizeRangesAsync( + appWidgetInfo.appWidgetId, + lapi, + app.context, + appWidgetInfo.spanX, + appWidgetInfo.spanY + ) + } + + if (shouldUpdate) { + c.updater() + .put(Favorites.APPWIDGET_PROVIDER, component.flattenToString()) + .put(Favorites.APPWIDGET_ID, appWidgetInfo.appWidgetId) + .put(Favorites.RESTORED, appWidgetInfo.restoreStatus) + .commit() + } + if (lapi != null) { + widgetProvidersMap[ComponentKey(lapi.provider, lapi.user)] = inflationResult.widgetInfo + if (appWidgetInfo.spanX < lapi.minSpanX || appWidgetInfo.spanY < lapi.minSpanY) { + FileLog.d( + TAG, + "Widget ${lapi.component} minSizes not meet: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}" + ) + logWidgetInfo(app.invariantDeviceProfile, lapi) + } + } + c.checkAndAddItem(appWidgetInfo, bgDataModel) + } + + companion object { + private const val TAG = "WorkspaceItemProcessor" + + private fun logWidgetInfo( + idp: InvariantDeviceProfile, + widgetProviderInfo: LauncherAppWidgetProviderInfo + ) { + val cellSize = Point() + for (deviceProfile in idp.supportedProfiles) { + deviceProfile.getCellSize(cellSize) + FileLog.d( + TAG, + "DeviceProfile available width: ${deviceProfile.availableWidthPx}," + + " available height: ${deviceProfile.availableHeightPx}," + + " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," + + " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," + + " cellSize: $cellSize" + ) + } + val widgetDimension = StringBuilder() + widgetDimension + .append("Widget dimensions:\n") + .append("minResizeWidth: ") + .append(widgetProviderInfo.minResizeWidth) + .append("\n") + .append("minResizeHeight: ") + .append(widgetProviderInfo.minResizeHeight) + .append("\n") + .append("defaultWidth: ") + .append(widgetProviderInfo.minWidth) + .append("\n") + .append("defaultHeight: ") + .append(widgetProviderInfo.minHeight) + .append("\n") + if (Utilities.ATLEAST_S) { + widgetDimension + .append("targetCellWidth: ") + .append(widgetProviderInfo.targetCellWidth) + .append("\n") + .append("targetCellHeight: ") + .append(widgetProviderInfo.targetCellHeight) + .append("\n") + .append("maxResizeWidth: ") + .append(widgetProviderInfo.maxResizeWidth) + .append("\n") + .append("maxResizeHeight: ") + .append(widgetProviderInfo.maxResizeHeight) + .append("\n") + } + FileLog.d(TAG, widgetDimension.toString()) + } + } +} diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java index 6c2f5890be..210d720bca 100644 --- a/src/com/android/launcher3/model/data/AppInfo.java +++ b/src/com/android/launcher3/model/data/AppInfo.java @@ -30,16 +30,20 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.pm.PackageInstallInfo; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.util.UserIconInfo; import java.util.Comparator; /** * Represents an app in AllAppsView. */ +@SuppressWarnings("NewApi") public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { public static final AppInfo[] EMPTY_ARRAY = new AppInfo[0]; @@ -48,12 +52,16 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { return uc != 0 ? uc : a.componentName.compareTo(b.componentName); }; + public static final Comparator PACKAGE_KEY_COMPARATOR = Comparator.comparingInt( + (AppInfo a) -> a.user.hashCode()).thenComparing(ItemInfo::getTargetPackage); + /** * The intent used to start the application. */ public Intent intent; - @NonNull + // componentName for the Private Space Install App button can be null + @Nullable public ComponentName componentName; // Section name used for indexing. @@ -80,20 +88,21 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { * Must not hold the Context. */ public AppInfo(Context context, LauncherActivityInfo info, UserHandle user) { - this(info, user, context.getSystemService(UserManager.class).isQuietModeEnabled(user)); + this(info, UserCache.INSTANCE.get(context).getUserInfo(user), + context.getSystemService(UserManager.class).isQuietModeEnabled(user)); } - public AppInfo(LauncherActivityInfo info, UserHandle user, boolean quietModeEnabled) { + public AppInfo(LauncherActivityInfo info, UserIconInfo userIconInfo, boolean quietModeEnabled) { this.componentName = info.getComponentName(); this.container = CONTAINER_ALL_APPS; - this.user = user; + this.user = userIconInfo.user; intent = makeLaunchIntent(info); if (quietModeEnabled) { runtimeStatusFlags |= FLAG_DISABLED_QUIET_USER; } uid = info.getApplicationInfo().uid; - updateRuntimeFlagsForActivityTarget(this, info); + updateRuntimeFlagsForActivityTarget(this, info, userIconInfo); } public AppInfo(AppInfo info) { @@ -167,14 +176,23 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { } public static void updateRuntimeFlagsForActivityTarget( - ItemInfoWithIcon info, LauncherActivityInfo lai) { + ItemInfoWithIcon info, LauncherActivityInfo lai, UserIconInfo userIconInfo) { ApplicationInfo appInfo = lai.getApplicationInfo(); if (PackageManagerHelper.isAppSuspended(appInfo)) { info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; } + if (Utilities.enableSupportForArchiving() && lai.getActivityInfo().isArchived) { + info.runtimeStatusFlags |= FLAG_ARCHIVED; + } info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES; + if (Flags.privateSpaceRestrictAccessibilityDrag()) { + if (userIconInfo.isPrivate()) { + info.runtimeStatusFlags |= FLAG_NOT_PINNABLE; + } + } + // Sets the progress level, installation and incremental download flags. info.setProgressLevel( PackageManagerHelper.getLoadingProgress(lai), diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java index 5b541d045b..83ba2b3a9f 100644 --- a/src/com/android/launcher3/model/data/FolderInfo.java +++ b/src/com/android/launcher3/model/data/FolderInfo.java @@ -371,4 +371,13 @@ public class FolderInfo extends ItemInfo { } return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } + + @Override + public boolean isDisabled() { + if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) { + return contents.stream().anyMatch((WorkspaceItemInfo::isDisabled)); + } + + return super.isDisabled(); + } } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 86393a0012..55849c2149 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -159,6 +159,13 @@ public class ItemInfo { @Nullable public CharSequence title; + /** + * Optionally set: The appTitle might e.g. be different if {@code title} is used to + * display progress (e.g. Downloading..). + */ + @Nullable + public CharSequence appTitle; + /** * Content description of the item. */ diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java index dc180d842e..352c3633ac 100644 --- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java @@ -18,15 +18,17 @@ package com.android.launcher3.model.data; import android.content.Context; import android.content.Intent; +import android.os.Process; import androidx.annotation.Nullable; +import com.android.launcher3.Utilities; 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; -import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.uioverrides.ApiWrapper; /** * Represents an ItemInfo which also holds an icon. @@ -112,6 +114,17 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ public static final int FLAG_NOT_PINNABLE = 1 << 13; + /** + * Flag indicating whether the package related to the item & user corresponds to that of + * archived app. + */ + public static final int FLAG_ARCHIVED = 1 << 14; + + /** + * Flag indicating it's the Private Space Install App icon. + */ + public static final int FLAG_PRIVATE_SPACE_INSTALL_APP = 1 << 15; + /** * 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. @@ -126,7 +139,8 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ private int mProgressLevel = 100; - protected ItemInfoWithIcon() { } + protected ItemInfoWithIcon() { + } protected ItemInfoWithIcon(ItemInfoWithIcon info) { super(info); @@ -141,6 +155,28 @@ public abstract class ItemInfoWithIcon extends ItemInfo { return (runtimeStatusFlags & FLAG_DISABLED_MASK) != 0; } + /** + * @return {@code true} if the app is pending download (0 progress) or if the app is archived + * and its install session is active + */ + public boolean isPendingDownload() { + if (isArchived()) { + return this.getProgressLevel() == 0 + && (this.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0; + } + return getProgressLevel() == 0; + } + + /** + * Returns true if the app corresponding to the item is archived. + */ + public boolean isArchived() { + if (!Utilities.enableSupportForArchiving()) { + return false; + } + return (runtimeStatusFlags & FLAG_ARCHIVED) != 0; + } + /** * Indicates whether we're using a low res icon */ @@ -157,7 +193,7 @@ public abstract class ItemInfoWithIcon extends ItemInfo { public boolean isAppStartable() { return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0) && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) - || mProgressLevel == 100); + || mProgressLevel == 100 || isArchived()); } /** @@ -166,7 +202,10 @@ public abstract class ItemInfoWithIcon extends ItemInfo { * progress. */ public int getProgressLevel() { - if ((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { + if (((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) + // This condition for archived apps is so that in case unarchival/update of + // archived app is cancelled, the state transitions back to 0% installed state. + || isArchived()) { return mProgressLevel; } return 100; @@ -216,7 +255,8 @@ public abstract class ItemInfoWithIcon extends ItemInfo { String targetPackage = getTargetPackage(); return targetPackage != null - ? new PackageManagerHelper(context).getMarketIntent(targetPackage) + ? ApiWrapper.getAppMarketActivityIntent( + context, targetPackage, Process.myUserHandle()) : null; } diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java index 1fbe04f84d..6fa8c546c9 100644 --- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java @@ -32,13 +32,11 @@ import android.os.Process; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.util.WidgetSizes; /** * Represents a widget (either instantiated or about to be) in the Launcher. @@ -143,8 +141,6 @@ public class LauncherAppWidgetInfo extends ItemInfo { */ private int widgetFeatures; - private boolean mHasNotifiedInitialWidgetSizeChanged; - /** * The container from which this widget was added (e.g. widgets tray, pin widget, search) */ @@ -202,17 +198,6 @@ public class LauncherAppWidgetInfo extends ItemInfo { .put(LauncherSettings.Favorites.APPWIDGET_SOURCE, sourceContainer); } - /** - * When we bind the widget, we should notify the widget that the size has changed if we have not - * done so already (only really for default workspace widgets). - */ - public void onBindAppWidget(Launcher launcher, AppWidgetHostView hostView) { - if (!mHasNotifiedInitialWidgetSizeChanged) { - WidgetSizes.updateWidgetSizeRanges(hostView, launcher, spanX, spanY); - mHasNotifiedInitialWidgetSizeChanged = true; - } - } - @Override protected String dumpProperties() { return super.dumpProperties() diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java index 3ce194dd81..9917ad7a64 100644 --- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java +++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java @@ -25,10 +25,12 @@ import android.text.TextUtils; import androidx.annotation.NonNull; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; import com.android.launcher3.icons.IconCache; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.ContentWriter; @@ -72,6 +74,12 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { */ public static final int FLAG_START_FOR_RESULT = 1 << 4; + /** + * The app is flagged non-resizeable, meaning that it does not support multi-window on small + * screens. + */ + public static final int FLAG_NON_RESIZEABLE = 1 << 5; + /** * The intent used to start the application. */ @@ -120,6 +128,11 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { public WorkspaceItemInfo(ShortcutInfo shortcutInfo, Context context) { user = shortcutInfo.getUserHandle(); itemType = Favorites.ITEM_TYPE_DEEP_SHORTCUT; + if (Flags.privateSpaceRestrictAccessibilityDrag()) { + if (UserCache.INSTANCE.get(context).getUserInfo(user).isPrivate()) { + runtimeStatusFlags |= FLAG_NOT_PINNABLE; + } + } updateFromDeepShortcutInfo(shortcutInfo, context); } @@ -148,9 +161,19 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { public final boolean isPromise() { - return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON); + return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON) + // For archived apps, promise icons are always ready to be displayed. + || isArchived(); } + /** + * Returns true if the workspace item supports promise icon UI. There are a few cases where they + * are supported: + * 1. Icons to be restored via backup/restore. + * 2. Icons added as an auto-install app. + * 3. Icons added due to it being an active install session created by the user. + * 4. Icons for archived apps. + */ public boolean hasPromiseIconUi() { return isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI); } @@ -172,8 +195,7 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { runtimeStatusFlags |= FLAG_DISABLED_BY_PUBLISHER; } disabledMessage = shortcutInfo.getDisabledMessage(); - if (Utilities.ATLEAST_P - && shortcutInfo.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { + if (shortcutInfo.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { runtimeStatusFlags |= FLAG_DISABLED_VERSION_LOWER; } else { runtimeStatusFlags &= ~FLAG_DISABLED_VERSION_LOWER; diff --git a/src/com/android/launcher3/notification/NotificationContainer.java b/src/com/android/launcher3/notification/NotificationContainer.java deleted file mode 100644 index 7cc9ad38bc..0000000000 --- a/src/com/android/launcher3/notification/NotificationContainer.java +++ /dev/null @@ -1,283 +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.notification; - -import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity; -import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.FloatProperty; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; - -import com.android.launcher3.R; -import com.android.launcher3.anim.AnimationSuccessListener; -import com.android.launcher3.popup.PopupContainerWithArrow; -import com.android.launcher3.touch.BaseSwipeDetector; -import com.android.launcher3.touch.OverScroll; -import com.android.launcher3.touch.SingleAxisSwipeDetector; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/** - * Class to manage the notification UI in a {@link PopupContainerWithArrow}. - * - * - Has two {@link NotificationMainView} that represent the top two notifications - * - Handles dismissing a notification - */ -public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener { - - private static final FloatProperty DRAG_TRANSLATION_X = - new FloatProperty("notificationProgress") { - @Override - public void setValue(NotificationContainer view, float transX) { - view.setDragTranslationX(transX); - } - - @Override - public Float get(NotificationContainer view) { - return view.mDragTranslationX; - } - }; - - private static final Rect sTempRect = new Rect(); - - private final SingleAxisSwipeDetector mSwipeDetector; - private final List mNotificationInfos = new ArrayList<>(); - private boolean mIgnoreTouch = false; - - private final ObjectAnimator mContentTranslateAnimator; - private float mDragTranslationX = 0; - - private final NotificationMainView mPrimaryView; - private final NotificationMainView mSecondaryView; - private PopupContainerWithArrow mPopupContainer; - - public NotificationContainer(Context context) { - this(context, null, 0); - } - - public NotificationContainer(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL); - mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false); - mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0); - - mPrimaryView = (NotificationMainView) View.inflate(getContext(), - R.layout.notification_content, null); - mSecondaryView = (NotificationMainView) View.inflate(getContext(), - R.layout.notification_content, null); - mSecondaryView.setAlpha(0); - - addView(mSecondaryView); - addView(mPrimaryView); - - } - - public void setPopupView(PopupContainerWithArrow popupView) { - mPopupContainer = popupView; - } - - /** - * Returns true if we should intercept the swipe. - */ - public boolean onInterceptSwipeEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - sTempRect.set(getLeft(), getTop(), getRight(), getBottom()); - mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY()); - if (!mIgnoreTouch) { - mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true); - } - } - if (mIgnoreTouch) { - return false; - } - if (mPrimaryView.getNotificationInfo() == null) { - // The notification hasn't been populated yet. - return false; - } - - mSwipeDetector.onTouchEvent(ev); - return mSwipeDetector.isDraggingOrSettling(); - } - - /** - * Returns true when we should handle the swipe. - */ - public boolean onSwipeEvent(MotionEvent ev) { - if (mIgnoreTouch) { - return false; - } - if (mPrimaryView.getNotificationInfo() == null) { - // The notification hasn't been populated yet. - return false; - } - return mSwipeDetector.onTouchEvent(ev); - } - - /** - * Applies the list of @param notificationInfos to this container. - */ - public void applyNotificationInfos(final List notificationInfos) { - mNotificationInfos.clear(); - if (notificationInfos.isEmpty()) { - mPrimaryView.applyNotificationInfo(null); - mSecondaryView.applyNotificationInfo(null); - return; - } - mNotificationInfos.addAll(notificationInfos); - - NotificationInfo mainNotification = notificationInfos.get(0); - mPrimaryView.applyNotificationInfo(mainNotification); - mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1 - ? notificationInfos.get(1) - : null); - } - - /** - * Trims the notifications. - * @param notificationKeys List of all valid notification keys. - */ - public void trimNotifications(final List notificationKeys) { - Iterator iterator = mNotificationInfos.iterator(); - while (iterator.hasNext()) { - if (!notificationKeys.contains(iterator.next().notificationKey)) { - iterator.remove(); - } - } - - NotificationInfo primaryInfo = mNotificationInfos.size() > 0 - ? mNotificationInfos.get(0) - : null; - NotificationInfo secondaryInfo = mNotificationInfos.size() > 1 - ? mNotificationInfos.get(1) - : null; - - mPrimaryView.applyNotificationInfo(primaryInfo); - mSecondaryView.applyNotificationInfo(secondaryInfo); - - mPrimaryView.onPrimaryDrag(0); - mSecondaryView.onSecondaryDrag(0); - } - - private void setDragTranslationX(float translationX) { - mDragTranslationX = translationX; - - float progress = translationX / getWidth(); - mPrimaryView.onPrimaryDrag(progress); - if (mSecondaryView.getNotificationInfo() == null) { - mSecondaryView.setAlpha(0f); - } else { - mSecondaryView.onSecondaryDrag(progress); - } - } - - // SingleAxisSwipeDetector.Listener's - @Override - public void onDragStart(boolean start, float startDisplacement) { - mPopupContainer.showArrow(false); - } - - @Override - public boolean onDrag(float displacement) { - if (!mPrimaryView.canChildBeDismissed()) { - displacement = OverScroll.dampedScroll(displacement, getWidth()); - } - - float progress = displacement / getWidth(); - mPrimaryView.onPrimaryDrag(progress); - if (mSecondaryView.getNotificationInfo() == null) { - mSecondaryView.setAlpha(0f); - } else { - mSecondaryView.onSecondaryDrag(progress); - } - mContentTranslateAnimator.cancel(); - return true; - } - - @Override - public void onDragEnd(float velocity) { - final boolean willExit; - final float endTranslation; - final float startTranslation = mPrimaryView.getTranslationX(); - final float width = getWidth(); - - if (!mPrimaryView.canChildBeDismissed()) { - willExit = false; - endTranslation = 0; - } else if (mSwipeDetector.isFling(velocity)) { - willExit = true; - endTranslation = velocity < 0 ? -width : width; - } else if (Math.abs(startTranslation) > width / 2f) { - willExit = true; - endTranslation = (startTranslation < 0 ? -width : width); - } else { - willExit = false; - endTranslation = 0; - } - - long duration = BaseSwipeDetector.calculateDuration(velocity, - (endTranslation - startTranslation) / width); - - mContentTranslateAnimator.removeAllListeners(); - mContentTranslateAnimator.setDuration(duration) - .setInterpolator(scrollInterpolatorForVelocity(velocity)); - mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation); - - NotificationMainView current = mPrimaryView; - mContentTranslateAnimator.addListener(new AnimationSuccessListener() { - @Override - public void onAnimationSuccess(Animator animator) { - mSwipeDetector.finishedScrolling(); - if (willExit) { - current.onChildDismissed(); - } - mPopupContainer.showArrow(true); - } - }); - mContentTranslateAnimator.start(); - } - - /** - * Animates the background color to a new color. - * @param color The color to change to. - * @param animatorSetOut The AnimatorSet where we add the color animator to. - */ - public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) { - mPrimaryView.updateBackgroundColor(color, animatorSetOut); - mSecondaryView.updateBackgroundColor(color, animatorSetOut); - } - - /** - * Updates the header with a new @param notificationCount. - */ - public void updateHeader(int notificationCount) { - mPrimaryView.updateHeader(notificationCount); - mSecondaryView.updateHeader(notificationCount - 1); - } -} diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java deleted file mode 100644 index f4468fdf01..0000000000 --- a/src/com/android/launcher3/notification/NotificationInfo.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package 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.Utilities.allowBGLaunch; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_LAUNCH_TAP; - -import android.app.ActivityOptions; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.service.notification.StatusBarNotification; -import android.view.View; - -import com.android.launcher3.AbstractFloatingView; -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 - * only be created when we need to show the notification contents on the UI; until then, a - * {@link DotInfo} with only the notification key should - * be passed around, and then this can be constructed using the StatusBarNotification from - * {@link NotificationListener#getNotificationsForKeys(java.util.List)}. - */ -public class NotificationInfo implements View.OnClickListener { - - public final PackageUserKey packageUserKey; - public final String notificationKey; - public final CharSequence title; - public final CharSequence text; - public final PendingIntent intent; - public final boolean autoCancel; - public final boolean dismissable; - - private final ItemInfo mItemInfo; - private Drawable mIconDrawable; - private int mIconColor; - private boolean mIsIconLarge; - - /** - * Extracts the data that we need from the StatusBarNotification. - */ - public NotificationInfo(Context context, StatusBarNotification statusBarNotification, - ItemInfo itemInfo) { - packageUserKey = PackageUserKey.fromNotification(statusBarNotification); - notificationKey = statusBarNotification.getKey(); - Notification notification = statusBarNotification.getNotification(); - title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); - text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); - - int iconType = notification.getBadgeIconType(); - // Load the icon. Since it is backed by ashmem, we won't copy the entire bitmap - // into our process as long as we don't touch it and it exists in systemui. - Icon icon = iconType == Notification.BADGE_ICON_SMALL ? null : notification.getLargeIcon(); - if (icon == null) { - // Use the small icon. - icon = notification.getSmallIcon(); - mIconDrawable = icon == null ? null : icon.loadDrawable(context); - mIconColor = statusBarNotification.getNotification().color; - mIsIconLarge = false; - } else { - // Use the large icon. - mIconDrawable = icon.loadDrawable(context); - mIsIconLarge = true; - } - if (mIconDrawable == null) { - mIconDrawable = LauncherAppState.getInstance(context).getIconCache() - .getDefaultIcon(statusBarNotification.getUser()).newIcon(context); - } - intent = notification.contentIntent; - autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0; - dismissable = (notification.flags & Notification.FLAG_ONGOING_EVENT) == 0; - this.mItemInfo = itemInfo; - } - - @Override - public void onClick(View view) { - if (intent == null) { - return; - } - final ActivityContext context = ActivityContext.lookupContext(view.getContext()); - ActivityOptions options = allowBGLaunch(ActivityOptions.makeClipRevealAnimation( - view, 0, 0, view.getWidth(), view.getHeight())); - try { - intent.send(null, 0, null, null, null, null, options.toBundle()); - context.getStatsLogManager().logger().withItemInfo(mItemInfo) - .log(LAUNCHER_NOTIFICATION_LAUNCH_TAP); - } catch (PendingIntent.CanceledException e) { - e.printStackTrace(); - } - if (autoCancel) { - PopupDataProvider popupDataProvider = context.getPopupDataProvider(); - if (popupDataProvider != null) { - popupDataProvider.cancelNotification(notificationKey); - } - } - AbstractFloatingView.closeOpenViews( - context, true, TYPE_ACTION_POPUP | TYPE_TASKBAR_ALL_APPS); - } - - public Drawable getIconForBackground(Context context, int background) { - if (mIsIconLarge) { - // Only small icons should be tinted. - return mIconDrawable; - } - mIconColor = IconPalette.resolveContrastColor(context, mIconColor, background); - Drawable icon = mIconDrawable.mutate(); - // DrawableContainer ignores the color filter if it's already set, so clear it first to - // get it set and invalidated properly. - icon.setTintList(null); - icon.setTint(mIconColor); - return icon; - } -} diff --git a/src/com/android/launcher3/notification/NotificationKeyData.java b/src/com/android/launcher3/notification/NotificationKeyData.java index 1dda3dfd27..4115b3da5c 100644 --- a/src/com/android/launcher3/notification/NotificationKeyData.java +++ b/src/com/android/launcher3/notification/NotificationKeyData.java @@ -26,13 +26,10 @@ import androidx.annotation.Nullable; import com.android.launcher3.Utilities; import java.util.ArrayList; -import java.util.List; /** * The key data associated with the notification, used to determine what to include * in dots and stub popup views before they are populated. - * - * @see NotificationInfo for the full data used when populating the stub views. */ public class NotificationKeyData { public final String notificationKey; @@ -56,15 +53,6 @@ public class NotificationKeyData { Notification.EXTRA_PEOPLE_LIST))); } - public static List extractKeysOnly( - @NonNull List notificationKeys) { - List keysOnly = new ArrayList<>(notificationKeys.size()); - for (NotificationKeyData notificationKeyData : notificationKeys) { - keysOnly.add(notificationKeyData.notificationKey); - } - return keysOnly; - } - private static String[] extractPersonKeyOnly(@Nullable ArrayList people) { if (people == null || people.isEmpty()) { return Utilities.EMPTY_STRING_ARRAY; diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index 04eb38a3c5..836ea4aefe 100644 --- a/src/com/android/launcher3/notification/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -34,7 +34,6 @@ 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; @@ -64,8 +63,7 @@ public class NotificationListener extends NotificationListenerService { private static final int MSG_NOTIFICATION_POSTED = 1; private static final int MSG_NOTIFICATION_REMOVED = 2; private static final int MSG_NOTIFICATION_FULL_REFRESH = 3; - private static final int MSG_CANCEL_NOTIFICATION = 4; - private static final int MSG_RANKING_UPDATE = 5; + private static final int MSG_RANKING_UPDATE = 4; private static NotificationListener sNotificationListenerInstance = null; private static final ArraySet sNotificationsChangedListeners = @@ -81,9 +79,6 @@ public class NotificationListener extends NotificationListenerService { /** Maps keys to their corresponding current group key */ private final Map mNotificationGroupKeyMap = new HashMap<>(); - /** The last notification key that was dismissed from launcher UI */ - private String mLastKeyDismissedByLauncher; - private SettingsCache mSettingsCache; private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; @@ -93,7 +88,7 @@ public class NotificationListener extends NotificationListenerService { sNotificationListenerInstance = this; } - public static @Nullable NotificationListener getInstanceIfConnected() { + private static @Nullable NotificationListener getInstanceIfConnected() { return sIsConnected ? sNotificationListenerInstance : null; } @@ -139,17 +134,9 @@ public class NotificationListener extends NotificationListenerService { if (notificationGroup != null) { notificationGroup.removeChildKey(key); if (notificationGroup.isEmpty()) { - if (key.equals(mLastKeyDismissedByLauncher)) { - // Only cancel the group notification if launcher dismissed the - // last child. - cancelNotification(notificationGroup.getGroupSummaryKey()); - } mNotificationGroupMap.remove(sbn.getGroupKey()); } } - if (key.equals(mLastKeyDismissedByLauncher)) { - mLastKeyDismissedByLauncher = null; - } return true; } case MSG_NOTIFICATION_FULL_REFRESH: @@ -164,11 +151,6 @@ public class NotificationListener extends NotificationListenerService { mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget(); return true; - case MSG_CANCEL_NOTIFICATION: { - mLastKeyDismissedByLauncher = (String) message.obj; - cancelNotification(mLastKeyDismissedByLauncher); - return true; - } case MSG_RANKING_UPDATE: { String[] keys = ((RankingMap) message.obj).getOrderedKeys(); for (StatusBarNotification sbn : getActiveNotificationsSafely(keys)) { @@ -272,14 +254,6 @@ public class NotificationListener extends NotificationListenerService { mWorkerHandler.obtainMessage(MSG_RANKING_UPDATE, rankingMap).sendToTarget(); } - /** - * Cancels a notification - */ - @AnyThread - public void cancelNotificationFromLauncher(String key) { - mWorkerHandler.obtainMessage(MSG_CANCEL_NOTIFICATION, key).sendToTarget(); - } - @WorkerThread private void updateGroupKeyIfNecessary(StatusBarNotification sbn) { String childKey = sbn.getKey(); @@ -314,15 +288,6 @@ public class NotificationListener extends NotificationListenerService { } } - /** - * This makes a potentially expensive binder call and should be run on a background thread. - */ - @WorkerThread - public List getNotificationsForKeys(List keys) { - return Arrays.asList(getActiveNotificationsSafely( - keys.stream().map(n -> n.notificationKey).toArray(String[]::new))); - } - /** * Returns true for notifications that have an intent and are not headers for grouped * notifications and should be shown in the notification popup. diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java deleted file mode 100644 index ecd018b26d..0000000000 --- a/src/com/android/launcher3/notification/NotificationMainView.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.notification; - -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.Utilities.mapToRange; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED; - -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.Outline; -import android.graphics.Rect; -import android.graphics.drawable.GradientDrawable; -import android.os.Build; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -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, - * e.g. icon + title + text. - */ -@TargetApi(Build.VERSION_CODES.N) -public class NotificationMainView extends LinearLayout { - - // This is used only to track the notification view, so that it can be properly logged. - public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo(); - - // Value when the primary notification main view will be gone (zero alpha). - private static final float PRIMARY_GONE_PROGRESS = 0.7f; - private static final float PRIMARY_MIN_PROGRESS = 0.40f; - private static final float PRIMARY_MAX_PROGRESS = 0.60f; - private static final float SECONDARY_MIN_PROGRESS = 0.30f; - private static final float SECONDARY_MAX_PROGRESS = 0.50f; - private static final float SECONDARY_CONTENT_MAX_PROGRESS = 0.6f; - - private NotificationInfo mNotificationInfo; - private int mBackgroundColor; - private TextView mTitleView; - private TextView mTextView; - private View mIconView; - - private View mHeader; - private View mMainView; - - private TextView mHeaderCount; - private final Rect mOutline = new Rect(); - - // Space between notifications during swipe - private final int mNotificationSpace; - private final int mMaxTransX; - private final int mMaxElevation; - - private final GradientDrawable mBackground; - - public NotificationMainView(Context context) { - this(context, null, 0); - } - - public NotificationMainView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public NotificationMainView(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); - } - - public NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes) { - super(context, attrs, defStyle, defStylRes); - - float outlineRadius = Themes.getDialogCornerRadius(context); - - mBackground = new GradientDrawable(); - mBackground.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary)); - mBackground.setCornerRadius(outlineRadius); - setBackground(mBackground); - - mMaxElevation = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_elevation); - setElevation(mMaxElevation); - - mMaxTransX = getResources().getDimensionPixelSize(R.dimen.notification_max_trans); - mNotificationSpace = getResources().getDimensionPixelSize(R.dimen.notification_space); - - setClipToOutline(true); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(mOutline, outlineRadius); - } - }); - } - - /** - * Updates the header text. - * @param notificationCount The number of notifications. - */ - public void updateHeader(int notificationCount) { - final String text; - final int visibility; - if (notificationCount <= 1) { - text = ""; - visibility = View.INVISIBLE; - } else { - text = String.valueOf(notificationCount); - visibility = View.VISIBLE; - - } - mHeaderCount.setText(text); - mHeaderCount.setVisibility(visibility); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - ViewGroup textAndBackground = findViewById(R.id.text_and_background); - mTitleView = textAndBackground.findViewById(R.id.title); - mTextView = textAndBackground.findViewById(R.id.text); - mIconView = findViewById(R.id.popup_item_icon); - mHeaderCount = findViewById(R.id.notification_count); - - mHeader = findViewById(R.id.header); - mMainView = findViewById(R.id.main_view); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mOutline.set(0, 0, getWidth(), getHeight()); - invalidateOutline(); - } - - private void updateBackgroundColor(int color) { - mBackgroundColor = color; - mBackground.setColor(color); - if (mNotificationInfo != null) { - mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), - mBackgroundColor)); - } - } - - /** - * Animates the background color to a new color. - * @param color The color to change to. - * @param animatorSetOut The AnimatorSet where we add the color animator to. - */ - public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) { - int oldColor = mBackgroundColor; - ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color); - colors.addUpdateListener(valueAnimator -> { - int newColor = (int) valueAnimator.getAnimatedValue(); - updateBackgroundColor(newColor); - }); - animatorSetOut.play(colors); - } - - /** - * Sets the content of this view, animating it after a new icon shifts up if necessary. - */ - public void applyNotificationInfo(NotificationInfo notificationInfo) { - mNotificationInfo = notificationInfo; - if (notificationInfo == null) { - return; - } - NotificationListener listener = NotificationListener.getInstanceIfConnected(); - if (listener != null) { - listener.setNotificationsShown(new String[] {mNotificationInfo.notificationKey}); - } - CharSequence title = mNotificationInfo.title; - CharSequence text = mNotificationInfo.text; - if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(text)) { - mTitleView.setText(title.toString()); - mTextView.setText(text.toString()); - } else { - mTitleView.setMaxLines(2); - mTitleView.setText(TextUtils.isEmpty(title) ? text.toString() : title.toString()); - mTextView.setVisibility(GONE); - } - mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), - mBackgroundColor)); - if (mNotificationInfo.intent != null) { - setOnClickListener(mNotificationInfo); - } - - // Add a stub ItemInfo so that logging populates the correct container and item types - // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively. - setTag(NOTIFICATION_ITEM_INFO); - } - - /** - * Sets the alpha of only the child views. - */ - public void setContentAlpha(float alpha) { - mHeader.setAlpha(alpha); - mMainView.setAlpha(alpha); - } - - /** - * Sets the translation of only the child views. - */ - public void setContentTranslationX(float transX) { - mHeader.setTranslationX(transX); - mMainView.setTranslationX(transX); - } - - /** - * Updates the alpha, content alpha, and elevation of this view. - * - * @param progress Range from [0, 1] or [-1, 0] - * When 0: Full alpha - * When 1/-1: zero alpha - */ - public void onPrimaryDrag(float progress) { - float absProgress = Math.abs(progress); - final int width = getWidth(); - - float min = PRIMARY_MIN_PROGRESS; - float max = PRIMARY_MAX_PROGRESS; - - if (absProgress < min) { - setAlpha(1f); - setContentAlpha(1); - setElevation(mMaxElevation); - } else if (absProgress < max) { - setAlpha(1f); - setContentAlpha(mapToRange(absProgress, min, max, 1f, 0f, LINEAR)); - setElevation(Utilities.mapToRange(absProgress, min, max, mMaxElevation, 0, LINEAR)); - } else { - setAlpha(mapToRange(absProgress, max, PRIMARY_GONE_PROGRESS, 1f, 0f, LINEAR)); - setContentAlpha(0f); - setElevation(0f); - } - - setTranslationX(width * progress); - } - - /** - * Updates the alpha, content alpha, elevation, and clipping of this view. - * @param progress Range from [0, 1] or [-1, 0] - * When 0: Smallest clipping, zero alpha - * When 1/-1: Full clip, full alpha - */ - public void onSecondaryDrag(float progress) { - final float absProgress = Math.abs(progress); - - float min = SECONDARY_MIN_PROGRESS; - float max = SECONDARY_MAX_PROGRESS; - float contentMax = SECONDARY_CONTENT_MAX_PROGRESS; - - if (absProgress < min) { - setAlpha(0f); - setContentAlpha(0); - setElevation(0f); - } else if (absProgress < max) { - setAlpha(mapToRange(absProgress, min, max, 0, 1f, LINEAR)); - setContentAlpha(0f); - setElevation(0f); - } else { - setAlpha(1f); - setContentAlpha(absProgress > contentMax - ? 1f - : mapToRange(absProgress, max, contentMax, 0, 1f, LINEAR)); - setElevation(Utilities.mapToRange(absProgress, max, 1, 0, mMaxElevation, LINEAR)); - } - - final int width = getWidth(); - int crop = (int) (width * absProgress); - int space = (int) (absProgress > PRIMARY_GONE_PROGRESS - ? mapToRange(absProgress, PRIMARY_GONE_PROGRESS, 1f, mNotificationSpace, 0, LINEAR) - : mNotificationSpace); - if (progress < 0) { - mOutline.left = Math.max(0, getWidth() - crop + space); - mOutline.right = getWidth(); - } else { - mOutline.right = Math.min(getWidth(), crop - space); - mOutline.left = 0; - } - - float contentTransX = mMaxTransX * (1f - absProgress); - setContentTranslationX(progress < 0 - ? contentTransX - : -contentTransX); - invalidateOutline(); - } - - public @Nullable NotificationInfo getNotificationInfo() { - return mNotificationInfo; - } - - public boolean canChildBeDismissed() { - return mNotificationInfo != null && mNotificationInfo.dismissable; - } - - public void onChildDismissed() { - 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 30156c8140..0640bf3672 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicator.java +++ b/src/com/android/launcher3/pageindicators/PageIndicator.java @@ -27,10 +27,12 @@ public interface PageIndicator { void setMarkersCount(int numMarkers); /** - * Sets flag to indicate when the screens are in the process of binding so that we don't animate - * during that period. + * Sets a flag indicating whether to pause scroll. + *

Should be set to {@code true} while the screen is binding or new data is being applied, + * and to {@code false} once done. This prevents animation conflicts due to scrolling during + * those periods.

*/ - default void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) { + default void setPauseScroll(boolean pause, boolean isTwoPanels) { // No-op by default } diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java index fd1b64faf9..77effca39f 100644 --- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java +++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java @@ -130,7 +130,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator */ private float mCurrentPosition; private float mFinalPosition; - private boolean mAreScreensBinding; + private boolean mIsScrollPaused; private boolean mIsTwoPanels; private ObjectAnimator mAnimator; private @Nullable ObjectAnimator mAlphaAnimator; @@ -172,7 +172,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator } // Skip scroll update during binding. We will update it when binding completes. - if (mAreScreensBinding) { + if (mIsScrollPaused) { return; } @@ -365,19 +365,26 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator @Override public void setMarkersCount(int numMarkers) { mNumPages = numMarkers; + + // If the last page gets removed we want to go to the previous page. + if (mNumPages > 0 && mNumPages == mActivePage) { + mActivePage--; + CURRENT_POSITION.set(this, (float) mActivePage); + } + requestLayout(); } @Override - public void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) { + public void setPauseScroll(boolean pause, boolean isTwoPanels) { mIsTwoPanels = isTwoPanels; // Reapply correct current position which was skipped during setScroll. - if (mAreScreensBinding && !areScreensBinding) { + if (mIsScrollPaused && !pause) { CURRENT_POSITION.set(this, (float) mActivePage); } - mAreScreensBinding = areScreensBinding; + mIsScrollPaused = pause; } @Override diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java index cb3c16c8aa..2ec994ed9a 100644 --- a/src/com/android/launcher3/pm/InstallSessionHelper.java +++ b/src/com/android/launcher3/pm/InstallSessionHelper.java @@ -22,7 +22,6 @@ import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; -import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; @@ -51,6 +50,7 @@ import java.util.Objects; /** * Utility class to tracking install sessions */ +@SuppressWarnings("NewApi") public class InstallSessionHelper { @NonNull @@ -129,7 +129,7 @@ public class InstallSessionHelper { public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) { for (SessionInfo info : getAllVerifiedSessions()) { boolean match = pkg.equals(info.getAppPackageName()); - if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) { + if (!user.equals(getUserHandle(info))) { match = false; } if (match) { @@ -177,9 +177,8 @@ public class InstallSessionHelper { @NonNull public List getAllVerifiedSessions() { - List list = new ArrayList<>(Utilities.ATLEAST_Q - ? Objects.requireNonNull(mLauncherApps).getAllPackageInstallerSessions() - : mInstaller.getAllSessions()); + List list = new ArrayList<>( + Objects.requireNonNull(mLauncherApps).getAllPackageInstallerSessions()); Iterator it = list.iterator(); while (it.hasNext()) { if (verify(it.next()) == null) { @@ -212,7 +211,8 @@ public class InstallSessionHelper { */ @WorkerThread void tryQueuePromiseAppIcon(@Nullable final PackageInstaller.SessionInfo sessionInfo) { - if (SessionCommitReceiver.isEnabled(mAppContext) + if (sessionInfo != null + && SessionCommitReceiver.isEnabled(mAppContext, getUserHandle(sessionInfo)) && verifySessionInfo(sessionInfo) && !promiseIconAddedForId(sessionInfo.getSessionId())) { FileLog.d(LOG, "Adding package name to install queue: " @@ -227,6 +227,12 @@ public class InstallSessionHelper { } public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) { + // For archived apps we always want to show promise icons and the checks below don't apply. + if (Utilities.enableSupportForArchiving() && sessionInfo != null + && sessionInfo.isUnarchival()) { + return true; + } + return verify(sessionInfo) != null && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER && sessionInfo.getAppIcon() != null @@ -244,6 +250,6 @@ public class InstallSessionHelper { } public static UserHandle getUserHandle(@NonNull final SessionInfo info) { - return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle(); + return info.getUser(); } } diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java index 41908d3496..eacbc11821 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -31,11 +31,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.android.launcher3.Utilities; import com.android.launcher3.util.PackageUserKey; import java.lang.ref.WeakReference; import java.util.Objects; +@SuppressWarnings("NewApi") @WorkerThread public class InstallSessionTracker extends PackageInstaller.SessionCallback { @@ -77,6 +79,13 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { } helper.tryQueuePromiseAppIcon(sessionInfo); + + if (Utilities.enableSupportForArchiving() && sessionInfo != null + && sessionInfo.isUnarchival()) { + // For archived apps, icon could already be present on the workspace. To make sure + // the icon state is updated, we send a change event. + callback.onPackageStateChanged(PackageInstallInfo.fromInstallingState(sessionInfo)); + } } @Override diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java index 4661fd4498..032de31ed7 100644 --- a/src/com/android/launcher3/pm/UserCache.java +++ b/src/com/android/launcher3/pm/UserCache.java @@ -28,8 +28,13 @@ import android.os.UserManager; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.UserBadgeDrawable; +import com.android.launcher3.util.FlagOp; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SimpleBroadcastReceiver; @@ -154,10 +159,25 @@ public class UserCache implements SafeCloseable { .orElse(Process.myUserHandle()); } + @VisibleForTesting + public void putToCache(UserHandle userHandle, UserIconInfo info) { + mUserToSerialMap.put(userHandle, info); + } + /** * @see UserManager#getUserProfiles() */ public List getUserProfiles() { return List.copyOf(mUserToSerialMap.keySet()); } + + /** + * Get a non-themed {@link UserBadgeDrawable} based on the provided {@link UserHandle}. + */ + @Nullable + public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) { + return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context) + .getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP)) + .getBadgeDrawable(context, false /* isThemed */); + } } diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index e3314d4b38..4d4a8f749a 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -636,10 +636,10 @@ public abstract class ArrowPopup return getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding); } - protected AnimatorSet getOpenCloseAnimator(boolean isOpening, int scaleDuration, - int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration, - Interpolator interpolator) { - + /** + * Sets X and Y pivots for the view animation considering arrow position. + */ + protected void setPivotForOpenCloseAnimation() { int arrowCenter = mArrowOffsetHorizontal + mArrowWidth / 2; if (mIsArrowRotated) { setPivotX(mIsLeftAligned ? 0f : getMeasuredWidth()); @@ -648,6 +648,14 @@ public abstract class ArrowPopup setPivotX(mIsLeftAligned ? arrowCenter : getMeasuredWidth() - arrowCenter); setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0f); } + } + + + protected AnimatorSet getOpenCloseAnimator(boolean isOpening, int scaleDuration, + int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration, + Interpolator interpolator) { + + setPivotForOpenCloseAnimation(); float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0}; float[] scaleValues = isOpening ? new float[] {0.5f, 1.02f} : new float[] {1f, 0.5f}; diff --git a/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java index c0a04b1302..89b5ba1b0c 100644 --- a/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java +++ b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java @@ -87,9 +87,4 @@ public class LauncherPopupLiveUpdateHandler extends PopupLiveUpdateHandler .collect(Collectors.toList()); container = (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( R.layout.popup_container, launcher.getDragLayer(), false); - container.configureForLauncher(launcher); + container.configureForLauncher(launcher, item); container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts); launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item)); container.requestFocus(); return container; } - private void configureForLauncher(Launcher launcher) { + private void configureForLauncher(Launcher launcher, ItemInfo itemInfo) { addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler( launcher, (PopupContainerWithArrow) this)); - mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this); + if (!Flags.privateSpaceRestrictItemDrag() + || !(itemInfo instanceof ItemInfoWithIcon itemInfoWithIcon) + || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0) { + mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this); + } mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher); launcher.getDragController().addDragListener(this); } @@ -248,10 +254,7 @@ public class PopupContainerWithArrow * Animates and loads shortcuts on background thread for this popup container */ private void loadAppShortcuts(ItemInfo originalItemInfo) { - - if (ATLEAST_P) { - setAccessibilityPaneTitle(getTitleForAccessibility()); - } + setAccessibilityPaneTitle(getTitleForAccessibility()); mOriginalIcon.setForceHideDot(true); // All views are added. Animate layout from now on. setLayoutTransition(new LayoutTransition()); diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index 44e3dd6390..fb463f7d24 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -31,17 +31,19 @@ import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.ShortcutUtil; +import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; +import com.android.launcher3.widget.picker.WidgetRecommendationCategory; import java.io.PrintWriter; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -73,7 +75,6 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan private void updateNotificationDots(Predicate updatedDots) { mNotificationDotsChangeListener.accept(updatedDots); - mChangeListener.onNotificationDotsUpdated(updatedDots); } @Override @@ -98,7 +99,6 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan mPackageUserToDotInfos.remove(removedPackageUserKey); } updateNotificationDots(removedPackageUserKey::equals); - trimNotifications(mPackageUserToDotInfos); } } @@ -136,11 +136,6 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan if (!updatedDots.isEmpty()) { updateNotificationDots(updatedDots::containsKey); } - trimNotifications(updatedDots); - } - - private void trimNotifications(Map updatedDots) { - mChangeListener.trimNotifications(updatedDots); } public void setDeepShortcutMap(HashMap deepShortcutMapCopy) { @@ -169,26 +164,23 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan if (dotInfo == null) { return null; } - List notifications = getNotificationsForItem( - info, dotInfo.getNotificationKeys()); - if (notifications.isEmpty()) { - return null; - } - return dotInfo; - } - public @NonNull List getNotificationKeysForItem(ItemInfo info) { - DotInfo dotInfo = getDotInfoForItem(info); - return dotInfo == null ? Collections.EMPTY_LIST - : getNotificationsForItem(info, dotInfo.getNotificationKeys()); - } - - public void cancelNotification(String notificationKey) { - NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); - if (notificationListener == null) { - return; + // If the item represents a pinned shortcut, ensure that there is a notification + // for this shortcut + String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info); + if (shortcutId == null) { + return dotInfo; } - notificationListener.cancelNotificationFromLauncher(notificationKey); + String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info); + return (dotInfo.getNotificationKeys().stream().anyMatch(notification -> { + if (notification.shortcutId != null) { + return notification.shortcutId.equals(shortcutId); + } + if (notification.personKeysFromNotification.length != 0) { + return Arrays.equals(notification.personKeysFromNotification, personKeys); + } + return false; + })) ? dotInfo : null; } /** @@ -229,6 +221,34 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan .collect(Collectors.toList()); } + /** Returns the recommended widgets mapped by their category. */ + @NonNull + public Map> getCategorizedRecommendedWidgets() { + Map allWidgetItems = mAllWidgets.stream() + .filter(entry -> entry instanceof WidgetsListContentEntry) + .flatMap(entry -> entry.mWidgets.stream()) + .distinct() + .collect(Collectors.toMap( + widget -> new ComponentKey(widget.componentName, widget.user), + Function.identity() + )); + return mRecommendedWidgets.stream() + .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo + && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null) + .collect(Collectors.groupingBy( + it -> ((PendingAddWidgetInfo) it).recommendationCategory, + Collectors.collectingAndThen( + Collectors.toList(), + list -> list.stream() + .map(it -> allWidgetItems.get( + new ComponentKey(it.getTargetComponent(), + it.user))) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ) + )); + } + public List getWidgetsForPackageUser(PackageUserKey packageUserKey) { return mAllWidgets.stream() .filter(row -> row instanceof WidgetsListContentEntry @@ -247,53 +267,18 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan .orElse(null); } - /** - * Returns a list of notifications that are relevant to given ItemInfo. - */ - public static @NonNull List getNotificationsForItem( - @NonNull ItemInfo info, @NonNull List notifications) { - String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info); - if (shortcutId == null) { - return notifications; - } - String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info); - return notifications.stream().filter((NotificationKeyData notification) -> { - if (notification.shortcutId != null) { - return notification.shortcutId.equals(shortcutId); - } - if (notification.personKeysFromNotification.length != 0) { - return Arrays.equals(notification.personKeysFromNotification, personKeys); - } - return false; - }).collect(Collectors.toList()); - } - public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "PopupDataProvider:"); 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() { }; - default void onNotificationDotsUpdated(Predicate updatedDots) { } - - default void trimNotifications(Map updatedDots) { } - default void onWidgetsBound() { } /** 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 index 9d6f2a5b51..4c94f9401f 100644 --- a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java +++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java @@ -18,7 +18,6 @@ package com.android.launcher3.popup; import android.content.Context; import android.view.View; -import com.android.launcher3.BubbleTextView; import com.android.launcher3.views.ActivityContext; /** @@ -56,12 +55,4 @@ public abstract class PopupLiveUpdateHandler { private static final String TAG = "RemoteActionShortcut"; private static final boolean DEBUG = Utilities.IS_DEBUG_DEVICE; @@ -119,9 +116,4 @@ public class RemoteActionShortcut extends SystemShortcut { .show(); } } - - @Override - public boolean isLeftGroup() { - return true; - } } diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java index 69bba6909b..0af7e677ed 100644 --- a/src/com/android/launcher3/popup/SystemShortcut.java +++ b/src/com/android/launcher3/popup/SystemShortcut.java @@ -1,33 +1,45 @@ package com.android.launcher3.popup; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP; import android.app.ActivityOptions; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Rect; +import android.os.Process; +import android.os.UserHandle; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; -import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.Launcher; +import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.SecondaryDropTarget; import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.PrivateProfileManager; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.uioverrides.ApiWrapper; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.WidgetsBottomSheet; +import java.util.Arrays; import java.util.List; /** @@ -35,12 +47,12 @@ import java.util.List; * onClickListener that depends on the item that the shortcut services. * * Example system shortcuts, defined as inner classes, include Widgets and AppInfo. - * @param + * + * @param extends {@link ActivityContext} */ -public abstract class SystemShortcut extends ItemInfo +public abstract class SystemShortcut extends ItemInfo implements View.OnClickListener { - private static final String TAG = SystemShortcut.class.getSimpleName(); private final int mIconResId; protected final int mLabelResId; protected int mAccessibilityActionId; @@ -49,11 +61,6 @@ public abstract class SystemShortcut extend 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, View originalView) { mIconResId = iconResId; @@ -73,24 +80,14 @@ public abstract class SystemShortcut extend mOriginalView = other.mOriginalView; } - /** - * Should be in the left group of icons in app's context menu header. - */ - public boolean isLeftGroup() { - return false; - } - 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) { @@ -98,36 +95,29 @@ public abstract class SystemShortcut extend mAccessibilityActionId, context.getText(mLabelResId)); } - public void setEnabled(boolean enabled) { - isEnabled = enabled; - } - - public boolean isEnabled() { - return isEnabled; - } - public boolean hasHandlerForAction(int action) { return mAccessibilityActionId == action; } - public interface Factory { + public interface Factory { - @Nullable SystemShortcut getShortcut(T activity, ItemInfo itemInfo, View originalView); + @Nullable + SystemShortcut getShortcut(T context, ItemInfo itemInfo, @NonNull View originalView); } - public static final Factory WIDGETS = (launcher, itemInfo, originalView) -> { + public static final Factory WIDGETS = (context, itemInfo, originalView) -> { if (itemInfo.getTargetComponent() == null) return null; final List widgets = - launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey( + context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey( itemInfo.getTargetComponent().getPackageName(), itemInfo.user)); if (widgets.isEmpty()) { return null; } - return new Widgets(launcher, itemInfo, originalView); + return new Widgets(context, itemInfo, originalView); }; - public static class Widgets extends SystemShortcut { - public Widgets(Launcher target, ItemInfo itemInfo, View originalView) { + public static class Widgets extends SystemShortcut { + public Widgets(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo, originalView); } @@ -144,14 +134,14 @@ public abstract class SystemShortcut extend } } - public static final Factory APP_INFO = AppInfo::new; + public static final Factory APP_INFO = AppInfo::new; - public static class AppInfo extends SystemShortcut { + public static class AppInfo extends SystemShortcut { @Nullable private SplitAccessibilityInfo mSplitA11yInfo; - public AppInfo(T target, ItemInfo itemInfo, View originalView) { + public AppInfo(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target, itemInfo, originalView); } @@ -190,7 +180,7 @@ public abstract class SystemShortcut extend public void onClick(View view) { dismissTaskMenuView(mTarget); Rect sourceBounds = Utilities.getViewBounds(view); - new PackageManagerHelper(mTarget).startDetailsActivityForInfo( + new PackageManagerHelper(view.getContext()).startDetailsActivityForInfo( mItemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle()); mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP); @@ -210,8 +200,83 @@ public abstract class SystemShortcut extend } } - public static final Factory INSTALL = + public static final Factory PRIVATE_PROFILE_INSTALL = + (context, itemInfo, originalView) -> { + if (originalView == null) { + return null; + } + if (itemInfo.getTargetComponent() == null + || !(itemInfo instanceof com.android.launcher3.model.data.AppInfo) + || !itemInfo.getContainerInfo().hasAllAppsContainer() + || !Process.myUserHandle().equals(itemInfo.user)) { + return null; + } + + PrivateProfileManager privateProfileManager = + context.getAppsView().getPrivateProfileManager(); + if (privateProfileManager == null || !privateProfileManager.isEnabled()) { + return null; + } + + UserHandle privateProfileUser = privateProfileManager.getProfileUser(); + if (privateProfileUser == null) { + return null; + } + // Do not show shortcut if an app is already installed to the space + ComponentName targetComponent = itemInfo.getTargetComponent(); + if (context.getAppsView().getAppsStore().getApp( + new ComponentKey(targetComponent, privateProfileUser)) != null) { + return null; + } + + // Do not show shortcut for settings + String[] packagesToSkip = + originalView.getContext().getResources() + .getStringArray(R.array.skip_private_profile_shortcut_packages); + if (Arrays.asList(packagesToSkip).contains(targetComponent.getPackageName())) { + return null; + } + + return new InstallToPrivateProfile<>( + context, itemInfo, originalView, privateProfileUser); + }; + + static class InstallToPrivateProfile extends SystemShortcut { + UserHandle mSpaceUser; + + InstallToPrivateProfile(T target, ItemInfo itemInfo, @NonNull View originalView, + UserHandle spaceUser) { + // TODO(b/302666597): update icon once available + super( + R.drawable.ic_install_to_private, + R.string.install_private_system_shortcut_label, + target, + itemInfo, + originalView); + mSpaceUser = spaceUser; + } + + @Override + public void onClick(View view) { + Intent intent = + ApiWrapper.getAppMarketActivityIntent( + view.getContext(), + mItemInfo.getTargetComponent().getPackageName(), + mSpaceUser); + mTarget.startActivitySafely(view, intent, mItemInfo); + AbstractFloatingView.closeAllOpenViews(mTarget); + mTarget.getStatsLogManager() + .logger() + .withItemInfo(mItemInfo) + .log(LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP); + } + } + + public static final Factory INSTALL = (activity, itemInfo, originalView) -> { + if (originalView == null) { + return null; + } boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) && ((WorkspaceItemInfo) itemInfo).hasStatusFlag( WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI); @@ -219,33 +284,103 @@ public abstract class SystemShortcut extend 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); + isInstantApp = InstantAppResolver.newInstance( + originalView.getContext()).isInstantApp(appInfo); } boolean enabled = supportsWebUI || isInstantApp; if (!enabled) { return null; } return new Install(activity, itemInfo, originalView); - }; + }; - public static class Install extends SystemShortcut { + public static class Install extends SystemShortcut { - public Install(BaseDraggingActivity target, ItemInfo itemInfo, View originalView) { + public Install(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label, target, itemInfo, originalView); } @Override public void onClick(View view) { - Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent( - mItemInfo.getTargetComponent().getPackageName()); + Intent intent = ApiWrapper.getAppMarketActivityIntent(view.getContext(), + mItemInfo.getTargetComponent().getPackageName(), + Process.myUserHandle()); mTarget.startActivitySafely(view, intent, mItemInfo); AbstractFloatingView.closeAllOpenViews(mTarget); } } - public static void dismissTaskMenuView(T activity) { + public static final Factory DONT_SUGGEST_APP = + (activity, itemInfo, originalView) -> { + if (!itemInfo.isPredictedItem()) { + return null; + } + return new DontSuggestApp<>(activity, itemInfo, originalView); + }; + + private static class DontSuggestApp extends SystemShortcut { + DontSuggestApp(T target, ItemInfo itemInfo, View originalView) { + super(R.drawable.ic_block_no_shadow, R.string.dismiss_prediction_label, target, + itemInfo, originalView); + } + + @Override + public void onClick(View view) { + dismissTaskMenuView(mTarget); + mTarget.getStatsLogManager().logger() + .withItemInfo(mItemInfo) + .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP); + } + } + + public static final Factory UNINSTALL_APP = + (activityContext, itemInfo, originalView) -> { + if (originalView == null) { + return null; + } + if (!Flags.enablePrivateSpace()) { + return null; + } + if (!UserCache.INSTANCE.get(originalView.getContext()).getUserInfo( + itemInfo.user).isPrivate()) { + // If app is not Private Space app. + return null; + } + ComponentName cn = SecondaryDropTarget.getUninstallTarget(originalView.getContext(), + itemInfo); + if (cn == null) { + // If component name is null, don't show uninstall shortcut. + // System apps will have component name as null. + return null; + } + return new UninstallApp(activityContext, itemInfo, originalView, cn); + }; + + private static class UninstallApp extends SystemShortcut { + @NonNull ComponentName mComponentName; + + UninstallApp(T target, ItemInfo itemInfo, @NonNull View originalView, + @NonNull ComponentName cn) { + super(R.drawable.ic_uninstall_no_shadow, R.string.uninstall_drop_target_label, target, + itemInfo, originalView); + mComponentName = cn; + + } + + @Override + public void onClick(View view) { + dismissTaskMenuView(mTarget); + SecondaryDropTarget.performUninstall(view.getContext(), mComponentName, mItemInfo); + mTarget.getStatsLogManager() + .logger() + .withItemInfo(mItemInfo) + .log(LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP); + } + } + + public static void dismissTaskMenuView(T activity) { AbstractFloatingView.closeOpenViews(activity, true, - AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); } } diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java index 30958d9c59..1f159471a0 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.java +++ b/src/com/android/launcher3/provider/LauncherDbUtils.java @@ -109,7 +109,7 @@ public class LauncherDbUtils { UserManagerState ums = new UserManagerState(); ums.init(UserCache.INSTANCE.get(context), context.getSystemService(UserManager.class)); - LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums); + LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, null); IntSet deletedShortcuts = new IntSet(); while (lc.moveToNext()) { diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index dc8cd3af65..22bc13bb25 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -18,11 +18,15 @@ package com.android.launcher3.provider; import static android.os.Process.myUserHandle; +import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed; import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; +import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; import static com.android.launcher3.provider.LauncherDbUtils.dropTable; import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; @@ -52,6 +56,8 @@ import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.DeviceGridState; import com.android.launcher3.model.LoaderTask; @@ -83,13 +89,15 @@ public class RestoreDbTask { private static final String TAG = "RestoreDbTask"; public static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; + public static final String FIRST_LOAD_AFTER_RESTORE_KEY = "first_load_after_restore"; private static final String INFO_COLUMN_NAME = "name"; private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value"; public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; public static final String APPWIDGET_IDS = "appwidget_ids"; - private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen", + @VisibleForTesting + public static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen", "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider", "appWidgetId", "restored"}; @@ -121,8 +129,11 @@ public class RestoreDbTask { FileLog.d(TAG, "performRestore: starting restore from db"); try (SQLiteTransaction t = new SQLiteTransaction(db)) { RestoreDbTask task = new RestoreDbTask(); - task.sanitizeDB(context, controller, db, new BackupManager(context)); - task.restoreAppWidgetIdsIfExists(context, controller); + BackupManager backupManager = new BackupManager(context); + LauncherRestoreEventLogger restoreEventLogger = + LauncherRestoreEventLogger.Companion.newInstance(context); + task.sanitizeDB(context, controller, db, backupManager, restoreEventLogger); + task.restoreAppWidgetIdsIfExists(context, controller, restoreEventLogger); t.commit(); return true; } catch (Exception e) { @@ -145,7 +156,8 @@ public class RestoreDbTask { */ @VisibleForTesting protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, - BackupManager backupManager) throws Exception { + BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger) + throws Exception { logFavoritesTable(db, "Old Launcher Database before sanitizing:", null, null); // Primary user ids long myProfileId = controller.getSerialNumberForUser(myUserHandle()); @@ -184,6 +196,9 @@ public class RestoreDbTask { Arrays.fill(args, "?"); final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")"; logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds); + if (enableLauncherBrMetricsFixed()) { + reportUnrestoredProfiles(db, where, profileIds, restoreEventLogger); + } int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds); FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted"); @@ -311,9 +326,6 @@ public class RestoreDbTask { */ private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber) { - if (!Utilities.ATLEAST_Q) { - return null; - } return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber); } @@ -340,23 +352,27 @@ public class RestoreDbTask { * Marks the DB state as pending restoration */ public static void setPending(Context context) { - FileLog.d(TAG, "Restore data received through full backup"); - LauncherPrefs.get(context) - .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType())); + DeviceGridState deviceGridState = new DeviceGridState(context); + FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState); + LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType())); + if (enableLauncherBrMetricsFixed()) { + LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true)); + } } @WorkerThread @VisibleForTesting - void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller) { + void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller, + LauncherRestoreEventLogger restoreEventLogger) { LauncherPrefs lp = LauncherPrefs.get(context); if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID); - restoreAppWidgetIds(context, controller, + restoreAppWidgetIds(context, controller, restoreEventLogger, IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), host); } else { - FileLog.d(TAG, "No app widget ids were received from backup to restore."); + FileLog.d(TAG, "Did not receive new app widget id map during Launcher restore"); } lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS); @@ -367,10 +383,13 @@ public class RestoreDbTask { */ @WorkerThread private void restoreAppWidgetIds(Context context, ModelDbController controller, - int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host) { + LauncherRestoreEventLogger launcherRestoreEventLogger, int[] oldWidgetIds, + int[] newWidgetIds, @NonNull AppWidgetHost host) { if (WidgetsModel.GO_DISABLE_WIDGETS) { FileLog.e(TAG, "Skipping widget ID remap as widgets not supported"); host.deleteHost(); + launcherRestoreEventLogger.logFavoritesItemsRestoreFailed(Favorites.ITEM_TYPE_APPWIDGET, + oldWidgetIds.length, RestoreError.WIDGETS_DISABLED); return; } if (!RestoreDbTask.isPending(context)) { @@ -434,11 +453,16 @@ public class RestoreDbTask { FileLog.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: " + oldWidgetId); host.deleteAppWidgetId(newWidgetIds[i]); + launcherRestoreEventLogger.logSingleFavoritesItemRestoreFailed( + ITEM_TYPE_APPWIDGET, + RestoreError.WIDGET_REMOVED + ); } } } } + logFavoritesTable(controller.getDb(), "launcher db after remap widget ids", null, null); LauncherAppState app = LauncherAppState.getInstanceNoCreate(); if (app != null) { app.getModel().forceReload(); @@ -473,17 +497,16 @@ public class RestoreDbTask { StringBuilder builder = new StringBuilder(); builder.append("["); for (int i = 0; i < widgetIdList.size(); i++) { - builder.append("[") + builder.append("[appWidgetId=") .append(widgetIdList.get(i)) - .append(", ") + .append(", restoreFlag=") .append(widgetRestoreList.get(i)) - .append(", ") + .append(", profileId=") .append(widgetProfileIdList.get(i)) .append("]"); } builder.append("]"); - Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: " - + builder); + Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: " + builder); } catch (Exception ex) { Log.e(TAG, "Getting widget ids from the database failed", ex); } @@ -542,7 +565,7 @@ public class RestoreDbTask { */ public static void logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader, String where, String[] profileIds) { - try (Cursor itemsToDelete = database.query( + try (Cursor cursor = database.query( /* table */ Favorites.TABLE_NAME, /* columns */ DB_COLUMNS_TO_LOG, /* selection */ where, @@ -551,26 +574,53 @@ public class RestoreDbTask { /* having */ null, /* orderBy */ null )) { - if (itemsToDelete.moveToFirst()) { - String[] columnNames = itemsToDelete.getColumnNames(); + if (cursor.moveToFirst()) { + String[] columnNames = cursor.getColumnNames(); StringBuilder stringBuilder = new StringBuilder(logHeader + "\n"); do { for (String columnName : columnNames) { stringBuilder.append(columnName) .append("=") - .append(itemsToDelete.getString( - itemsToDelete.getColumnIndex(columnName))) + .append(cursor.getString( + cursor.getColumnIndex(columnName))) .append(" "); } stringBuilder.append("\n"); - } while (itemsToDelete.moveToNext()); + } while (cursor.moveToNext()); FileLog.d(TAG, stringBuilder.toString()); } else { - FileLog.d(TAG, "logFavoritesTable: No items found from query for" + FileLog.d(TAG, "logFavoritesTable: No items found from query for " + "\"" + logHeader + "\""); } } catch (Exception e) { FileLog.e(TAG, "logFavoritesTable: Error reading from database", e); } } + + + /** + * Queries and reports the count of each itemType to be removed due to unrestored profiles. + * @param database The Launcher db to query from. + * @param where Query being used for to find unrestored profiles + * @param profileIds profile ids that were not restored + * @param restoreEventLogger Backup/Restore Logger to report metrics + */ + private void reportUnrestoredProfiles(SQLiteDatabase database, String where, + String[] profileIds, LauncherRestoreEventLogger restoreEventLogger) { + final String query = "SELECT itemType, COUNT(*) AS count FROM favorites WHERE " + + where + " GROUP BY itemType"; + try (Cursor cursor = database.rawQuery(query, profileIds)) { + if (cursor.moveToFirst()) { + do { + restoreEventLogger.logFavoritesItemsRestoreFailed( + cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow("count")), + RestoreError.PROFILE_NOT_RESTORED + ); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + FileLog.e(TAG, "reportUnrestoredProfiles: Error reading from database", e); + } + } } diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt index 45174a7095..43027da5fd 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -23,10 +23,10 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.android.launcher3.BubbleTextView import com.android.launcher3.allapps.BaseAllAppsAdapter import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.util.CancellableTask import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR import com.android.launcher3.views.ActivityContext -import java.util.concurrent.Future const val PREINFLATE_ICONS_ROW_COUNT = 4 const val EXTRA_ICONS_COUNT = 2 @@ -38,9 +38,8 @@ const val EXTRA_ICONS_COUNT = 2 */ class AllAppsRecyclerViewPool : RecycledViewPool() { - private var future: Future? = null - var hasWorkProfile = false + private var mCancellableTask: CancellableTask>? = null /** * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate. @@ -63,21 +62,40 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { override fun getLayoutManager(): RecyclerView.LayoutManager? = null } - // Inflate view holders on background thread, and added to view pool on main thread. - future?.cancel(true) - future = - VIEW_PREINFLATION_EXECUTOR.submit { - val viewHolders = - Array(preInflateCount) { - adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON) + mCancellableTask?.cancel() + var task: CancellableTask>? = null + task = + CancellableTask( + { + val list: ArrayList = ArrayList() + for (i in 0 until preInflateCount) { + if (task?.canceled == true) { + break + } + list.add( + adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON) + ) } - MAIN_EXECUTOR.execute { + list + }, + MAIN_EXECUTOR, + { viewHolders -> for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) { putRecycledView(viewHolders[i]) } } - null - } + ) + mCancellableTask = task + VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask) + } + + /** + * When clearing [RecycledViewPool], we should also abort pre-inflation tasks. This will make + * sure we don't inflate app icons after DeviceProfile has changed. + */ + override fun clear() { + super.clear() + mCancellableTask?.cancel() } /** diff --git a/src/com/android/launcher3/responsive/ResponsiveSpec.kt b/src/com/android/launcher3/responsive/ResponsiveSpec.kt index b0e1b27b5b..65e0b32cf5 100644 --- a/src/com/android/launcher3/responsive/ResponsiveSpec.kt +++ b/src/com/android/launcher3/responsive/ResponsiveSpec.kt @@ -59,7 +59,7 @@ data class ResponsiveSpec( val cellSize: SizeSpec, ) : IResponsiveSpec { init { - check(isValid()) { "Invalid ResponsiveSpec found." } + check(isValid()) { "Invalid ResponsiveSpec found. $this" } } constructor( @@ -106,7 +106,7 @@ data class ResponsiveSpec( } if (!isValidRemainderSpace()) { - logError("The total Remainder Space used must be lower or equal to 100%.") + logError("The total Remainder Space used must be equal to 0 or 1.") return false } @@ -131,11 +131,12 @@ data class ResponsiveSpec( } private fun isValidRemainderSpace(): Boolean { - // TODO(b/313621277): This validation must be update do accept only 0 or 1 instead of <= 1f. - return startPadding.ofRemainderSpace + - endPadding.ofRemainderSpace + - gutter.ofRemainderSpace + - cellSize.ofRemainderSpace <= 1f + val remainderSpaceUsed = + startPadding.ofRemainderSpace + + endPadding.ofRemainderSpace + + gutter.ofRemainderSpace + + cellSize.ofRemainderSpace + return remainderSpaceUsed == 0f || remainderSpaceUsed == 1f } private fun isValidAvailableSpace(): Boolean { @@ -254,8 +255,8 @@ class CalculatedResponsiveSpec { startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx) endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx) - gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx) - cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx) + gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx, gutters) + cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx, cells) } override fun hashCode(): Int { diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt index d14689853b..41dcd5e10b 100644 --- a/src/com/android/launcher3/responsive/SizeSpec.kt +++ b/src/com/android/launcher3/responsive/SizeSpec.kt @@ -57,11 +57,16 @@ data class SizeSpec( /** * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace * is 0, returns a default value. + * + * @param remainderSpace The remainder space to be used for the calculation + * @param defaultValue The default value to be returned when no ofRemainderSpace is defined + * @param factor A number to divide the remainder space. The default value is 1. This property + * is used to split equally the remainder space by the number of cells and gutters. */ - fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int { + fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int, factor: Int = 1): Int { val remainderSpaceValue = if (ofRemainderSpace > 0) { - (ofRemainderSpace * remainderSpace).roundToInt() + (ofRemainderSpace * remainderSpace / factor).roundToInt() } else { defaultValue } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java index 8d1d96b649..79b25a424f 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java @@ -16,6 +16,8 @@ package com.android.launcher3.secondarydisplay; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; + import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -129,6 +131,10 @@ public class SecondaryDragController extends DragController> { public static final String TAG = "StateManager"; + // b/279059025 + private static final boolean DEBUG = true; private final AnimationState mConfig = new AnimationState(); private final Handler mUiHandler; @@ -151,6 +156,13 @@ public class StateManager> { goToState(state, shouldAnimateStateChange()); } + /** + * @see #goToState(STATE_TYPE, boolean, AnimatorListener) + */ + public void goToState(STATE_TYPE state, AnimatorListener listener) { + goToState(state, shouldAnimateStateChange(), listener); + } + /** * @see #goToState(STATE_TYPE, boolean, AnimatorListener) */ @@ -163,7 +175,7 @@ public class StateManager> { * * @param animated false if the state should change immediately without any animation, * true otherwise - * @paras onCompleteRunnable any action to perform at the end of the transition, of null. + * @param listener any action to perform at the end of the transition, or null. */ public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) { goToState(state, animated, 0, listener); @@ -189,7 +201,7 @@ public class StateManager> { public void reapplyState(boolean cancelCurrentAnimation) { boolean wasInAnimation = mConfig.currentAnimation != null; - if (cancelCurrentAnimation) { + if (cancelCurrentAnimation && (mConfig.animProps & HANDLE_STATE_APPLY) == 0) { // Animation canceling can trigger a cleanup routine, causing problems when we are in a // launcher state that relies on member variable data. So if we are in one of those // states, accelerate the current animation to its end point rather than canceling it @@ -227,7 +239,18 @@ public class StateManager> { private void goToState( STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) { - Log.d(TestProtocol.OVERVIEW_OVER_HOME, "go to state " + state); + if (DEBUG) { + String stackTrace = Log.getStackTraceString(new Exception("tracing state transition")); + String truncatedTrace = + Arrays.stream(stackTrace.split("\\n")) + .limit(5) + .skip(1) // Removes the line "java.lang.Exception: tracing state + // transition" + .filter(traceLine -> !traceLine.contains("StateManager.goToState")) + .collect(Collectors.joining("\n")); + Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state + + ", partial trace:\n" + truncatedTrace); + } animated &= areAnimatorsEnabled(); if (mActivity.isInState(state)) { @@ -237,7 +260,7 @@ public class StateManager> { listener.onAnimationEnd(null); } return; - } else if ((!mConfig.userControlled && animated && mConfig.targetState == state) + } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state) || mState.shouldPreserveDataStateOnReapply()) { // We are running the same animation as requested, and/or target state should not be // reset -- allow the current animation to complete instead of canceling it. @@ -312,6 +335,20 @@ public class StateManager> { */ public AnimatorSet createAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { + if (DEBUG) { + String stackTrace = Log.getStackTraceString(new Exception("tracing state transition")); + String truncatedTrace = + Arrays.stream(stackTrace.split("\\n")) + .limit(5) + .skip(1) // Removes the line "java.lang.Exception: tracing state + // transition" + .filter(traceLine -> !traceLine.contains( + "StateManager.createAtomicAnimation")) + .collect(Collectors.joining("\n")); + Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState + + ", partial trace:\n" + truncatedTrace); + } + PendingAnimation builder = new PendingAnimation(config.duration); prepareForAtomicAnimation(fromState, toState, config); @@ -343,7 +380,7 @@ public class StateManager> { public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state, StateAnimationConfig config) { - config.userControlled = true; + config.animProps |= StateAnimationConfig.USER_CONTROLLED; cancelAnimation(); config.copyTo(mConfig); mConfig.playbackController = createAnimationToNewWorkspaceInternal(state) @@ -383,8 +420,9 @@ public class StateManager> { mState = state; mActivity.onStateSetStart(mState); - Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying listeners for state transition start" - + " to state: " + state.toString()); + if (DEBUG) { + Log.d(TAG, "onStateTransitionStart - state: " + state); + } for (int i = mListeners.size() - 1; i >= 0; i--) { mListeners.get(i).onStateTransitionStart(state); } @@ -402,8 +440,9 @@ public class StateManager> { setRestState(null); } - Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying " + mListeners.size() + " listeners " - + "for end transition for state: " + state.toString()); + if (DEBUG) { + Log.d(TAG, "onStateTransitionEnd - state: " + state); + } for (int i = mListeners.size() - 1; i >= 0; i--) { mListeners.get(i).onStateTransitionComplete(state); } @@ -418,7 +457,7 @@ public class StateManager> { } public void moveToRestState(boolean isAnimated) { - if (mConfig.currentAnimation != null && mConfig.userControlled) { + if (mConfig.currentAnimation != null && mConfig.isUserControlled()) { // The user is doing something. Lets not mess it up return; } @@ -441,7 +480,9 @@ public class StateManager> { * Cancels the current animation. */ public void cancelAnimation() { - Log.d(TestProtocol.OVERVIEW_OVER_HOME, "current animation cancelled"); + if (DEBUG && mConfig.currentAnimation != null) { + Log.d(TAG, "cancelAnimation - with ongoing animation"); + } mConfig.reset(); // It could happen that a new animation is set as a result of an endListener on the // existing animation. @@ -450,10 +491,18 @@ public class StateManager> { } } + /** + * Sets the provided controller as the current user controlled state animation + */ public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) { + setCurrentAnimation(controller, StateAnimationConfig.USER_CONTROLLED); + } + + public void setCurrentAnimation(AnimatorPlaybackController controller, + @AnimationPropertyFlags int animationProps) { clearCurrentAnimation(); setCurrentAnimation(controller.getTarget()); - mConfig.userControlled = true; + mConfig.animProps = animationProps; mConfig.playbackController = controller; } @@ -465,7 +514,6 @@ public class StateManager> { * @param toState The state we are animating towards. */ public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) { - Log.d(TestProtocol.OVERVIEW_OVER_HOME, "setting animation to " + toState.toString()); cancelAnimation(); setCurrentAnimation(anim); anim.addListener(createStateAnimationListener(toState)); diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java index 520f33ca74..30ba703fd7 100644 --- a/src/com/android/launcher3/statemanager/StatefulActivity.java +++ b/src/com/android/launcher3/statemanager/StatefulActivity.java @@ -126,16 +126,8 @@ public abstract class StatefulActivity> @Override public void reapplyUi() { - reapplyUi(true /* cancelCurrentAnimation */); - } - - /** - * Re-applies if any state transition is not running, optionally cancelling - * the transition if requested. - */ - public void reapplyUi(boolean cancelCurrentAnimation) { getRootView().dispatchInsets(); - getStateManager().reapplyState(cancelCurrentAnimation); + getStateManager().reapplyState(true /* cancelCurrentAnimation */); } @Override diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java index 0d9e01035e..0ca5afd1e8 100644 --- a/src/com/android/launcher3/states/StateAnimationConfig.java +++ b/src/com/android/launcher3/states/StateAnimationConfig.java @@ -40,8 +40,19 @@ public class StateAnimationConfig { public static final int SKIP_DEPTH_CONTROLLER = 1 << 2; public static final int SKIP_SCRIM = 1 << 3; + @IntDef(flag = true, value = { + USER_CONTROLLED, + HANDLE_STATE_APPLY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AnimationPropertyFlags {} + // Indicates that the animation is controlled by the user + public static final int USER_CONTROLLED = 1 << 0; + // Indicates that he animation can survive state UI resets due to inset or config changes + public static final int HANDLE_STATE_APPLY = 1 << 1; + public long duration; - public boolean userControlled; + public @AnimationPropertyFlags int animProps = 0; public @AnimationFlags int animFlags = 0; @@ -105,12 +116,16 @@ public class StateAnimationConfig { public void copyTo(StateAnimationConfig target) { target.duration = duration; target.animFlags = animFlags; - target.userControlled = userControlled; + target.animProps = animProps; for (int i = 0; i < ANIM_TYPES_COUNT; i++) { target.mInterpolators[i] = mInterpolators[i]; } } + public boolean isUserControlled() { + return (animProps & USER_CONTROLLED) != 0; + } + /** * Returns the interpolator set for animId or fallback if nothing is set * diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index 2c834bdb3f..1231cd7041 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -15,19 +15,20 @@ */ package com.android.launcher3.testing; -import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST; import static com.android.launcher3.Flags.enableGridOnlyOverview; +import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST; +import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE; +import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import android.annotation.TargetApi; 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; @@ -60,7 +61,6 @@ import java.util.function.Supplier; /** * Class to handle requests from tests */ -@TargetApi(Build.VERSION_CODES.Q) public class TestInformationHandler implements ResourceBasedOverride { public static TestInformationHandler newInstance(Context context) { @@ -153,11 +153,19 @@ public class TestInformationHandler implements ResourceBasedOverride { }, this::getCurrentActivity); } - case TestProtocol.REQUEST_IME_INSETS: { + case TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT: { + response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, + mDeviceProfile.cellLayoutBorderSpacePx.y); + return response; + } + + case TestProtocol.REQUEST_SYSTEM_GESTURE_REGION: { return getUIProperty(Bundle::putParcelable, activity -> { WindowInsetsCompat insets = WindowInsetsCompat.toWindowInsetsCompat( activity.getWindow().getDecorView().getRootWindowInsets()); - return insets.getInsets(WindowInsetsCompat.Type.ime()).toPlatformInsets(); + return insets.getInsets(WindowInsetsCompat.Type.ime() + | WindowInsetsCompat.Type.systemGestures()) + .toPlatformInsets(); }, this::getCurrentActivity); } @@ -175,6 +183,11 @@ public class TestInformationHandler implements ResourceBasedOverride { response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, mDeviceProfile.isTablet); return response; + case TestProtocol.REQUEST_ENABLE_TASKBAR_NAVBAR_UNIFICATION: + response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, + ENABLE_TASKBAR_NAVBAR_UNIFICATION); + return response; + case TestProtocol.REQUEST_NUM_ALL_APPS_COLUMNS: response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mDeviceProfile.numShownAllAppsColumns); @@ -203,6 +216,11 @@ public class TestInformationHandler implements ResourceBasedOverride { return response; } + case TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE: + response.putBoolean(TEST_INFO_RESPONSE_FIELD, enableSplitContextually() + && Launcher.ACTIVITY_TRACKER.getCreatedActivity().isSplitSelectionActive()); + return response; + case TestProtocol.REQUEST_ENABLE_ROTATION: MAIN_EXECUTOR.submit(() -> Launcher.ACTIVITY_TRACKER.getCreatedActivity().getRotationHelper() @@ -330,6 +348,7 @@ public class TestInformationHandler implements ResourceBasedOverride { return null; } T value = provider.apply(target); + Bundle response = new Bundle(); bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value); return response; diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java index 8b9bc19e18..fe4a83b8d0 100644 --- a/src/com/android/launcher3/touch/AllAppsSwipeController.java +++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java @@ -192,7 +192,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { protected StateAnimationConfig getConfigForStates(LauncherState fromState, LauncherState toState) { StateAnimationConfig config = super.getConfigForStates(fromState, toState); - config.userControlled = true; + config.animProps |= StateAnimationConfig.USER_CONTROLLED; if (fromState == NORMAL && toState == ALL_APPS) { applyNormalToAllAppsAnimConfig(mLauncher, config); } else if (fromState == ALL_APPS && toState == NORMAL) { @@ -209,13 +209,13 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { config.setInterpolator(ANIM_SCRIM_FADE, Interpolators.reverse(ALL_APPS_SCRIM_RESPONDER)); config.setInterpolator(ANIM_ALL_APPS_FADE, FINAL_FRAME); - if (!config.userControlled) { + if (!config.isUserControlled()) { config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED); } config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE); config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE); } else { - if (config.userControlled) { + if (config.isUserControlled()) { config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL)); config.setInterpolator(ANIM_WORKSPACE_FADE, Interpolators.reverse(WORKSPACE_FADE_MANUAL)); @@ -250,29 +250,32 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { if (launcher.getDeviceProfile().isTablet) { config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT); config.setInterpolator(ANIM_SCRIM_FADE, ALL_APPS_SCRIM_RESPONDER); - if (!config.userControlled) { + if (!config.isUserControlled()) { config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED); } config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE); config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE); } else { - config.setInterpolator(ANIM_DEPTH, config.userControlled ? BLUR_MANUAL : BLUR_ATOMIC); + config.setInterpolator(ANIM_DEPTH, + config.isUserControlled() ? BLUR_MANUAL : BLUR_ATOMIC); config.setInterpolator(ANIM_WORKSPACE_FADE, - config.userControlled ? WORKSPACE_FADE_MANUAL : WORKSPACE_FADE_ATOMIC); + config.isUserControlled() ? WORKSPACE_FADE_MANUAL : WORKSPACE_FADE_ATOMIC); config.setInterpolator(ANIM_WORKSPACE_SCALE, - config.userControlled ? WORKSPACE_SCALE_MANUAL : WORKSPACE_SCALE_ATOMIC); + config.isUserControlled() ? WORKSPACE_SCALE_MANUAL : WORKSPACE_SCALE_ATOMIC); config.setInterpolator(ANIM_HOTSEAT_FADE, - config.userControlled ? HOTSEAT_FADE_MANUAL : HOTSEAT_FADE_ATOMIC); + config.isUserControlled() ? HOTSEAT_FADE_MANUAL : HOTSEAT_FADE_ATOMIC); config.setInterpolator(ANIM_HOTSEAT_SCALE, - config.userControlled ? HOTSEAT_SCALE_MANUAL : HOTSEAT_SCALE_ATOMIC); + config.isUserControlled() ? HOTSEAT_SCALE_MANUAL : HOTSEAT_SCALE_ATOMIC); config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, - config.userControlled ? HOTSEAT_TRANSLATE_MANUAL : HOTSEAT_TRANSLATE_ATOMIC); + config.isUserControlled() + ? HOTSEAT_TRANSLATE_MANUAL + : HOTSEAT_TRANSLATE_ATOMIC); config.setInterpolator(ANIM_SCRIM_FADE, - config.userControlled ? SCRIM_FADE_MANUAL : SCRIM_FADE_ATOMIC); + config.isUserControlled() ? SCRIM_FADE_MANUAL : SCRIM_FADE_ATOMIC); config.setInterpolator(ANIM_ALL_APPS_FADE, - config.userControlled ? ALL_APPS_FADE_MANUAL : ALL_APPS_FADE_ATOMIC); + config.isUserControlled() ? ALL_APPS_FADE_MANUAL : ALL_APPS_FADE_ATOMIC); config.setInterpolator(ANIM_VERTICAL_PROGRESS, - config.userControlled + config.isUserControlled() ? ALL_APPS_VERTICAL_PROGRESS_MANUAL : ALL_APPS_VERTICAL_PROGRESS_ATOMIC); } @@ -285,7 +288,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { */ public static void applyOverviewToAllAppsAnimConfig( DeviceProfile deviceProfile, StateAnimationConfig config, float threshold) { - config.userControlled = true; + config.animProps |= StateAnimationConfig.USER_CONTROLLED; config.animFlags = SKIP_OVERVIEW; if (deviceProfile.isTablet) { config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT); diff --git a/src/com/android/launcher3/touch/DefaultPagedViewHandler.java b/src/com/android/launcher3/touch/DefaultPagedViewHandler.java new file mode 100644 index 0000000000..272ed10b5b --- /dev/null +++ b/src/com/android/launcher3/touch/DefaultPagedViewHandler.java @@ -0,0 +1,128 @@ +/* + * 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.touch; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +import com.android.launcher3.Utilities; + +public class DefaultPagedViewHandler implements PagedOrientationHandler { + @Override + public int getPrimaryValue(int x, int y) { + return x; + } + + @Override + public int getSecondaryValue(int x, int y) { + return y; + } + + @Override + public float getPrimaryValue(float x, float y) { + return x; + } + + @Override + public float getSecondaryValue(float x, float y) { + return y; + } + + @Override + public void setPrimary(T target, Int2DAction action, int param) { + action.call(target, param, 0); + } + + @Override + public void setPrimary(T target, Float2DAction action, float param) { + action.call(target, param, 0); + } + + @Override + public float getPrimaryDirection(MotionEvent event, int pointerIndex) { + return event.getX(pointerIndex); + } + + @Override + public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) { + return velocityTracker.getXVelocity(pointerId); + } + + @Override + public int getMeasuredSize(View view) { + return view.getMeasuredWidth(); + } + + @Override + public int getPrimaryScroll(View view) { + return view.getScrollX(); + } + + @Override + public float getPrimaryScale(View view) { + return view.getScaleX(); + } + + @Override + public void setMaxScroll(AccessibilityEvent event, int maxScroll) { + event.setMaxScrollX(maxScroll); + } + + @Override + public boolean getRecentsRtlSetting(Resources resources) { + return !Utilities.isRtl(resources); + } + + @Override + public int getChildStart(View view) { + return view.getLeft(); + } + + @Override + public int getCenterForPage(View view, Rect insets) { + return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top + - insets.bottom - view.getPaddingBottom()) / 2; + } + + @Override + public int getScrollOffsetStart(View view, Rect insets) { + return insets.left + view.getPaddingLeft(); + } + + @Override + public int getScrollOffsetEnd(View view, Rect insets) { + return view.getWidth() - view.getPaddingRight() - insets.right; + } + + @Override + public ChildBounds getChildBounds(View child, int childStart, int pageCenter, + boolean layoutChild) { + final int childWidth = child.getMeasuredWidth(); + final int childRight = childStart + childWidth; + final int childHeight = child.getMeasuredHeight(); + final int childTop = pageCenter - childHeight / 2; + if (layoutChild) { + child.layout(childStart, childTop, childRight, childTop + childHeight); + } + return new ChildBounds(childWidth, childHeight, childRight, childTop); + } + +} diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index a9c2a2e368..111931eeeb 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -18,6 +18,7 @@ package com.android.launcher3.touch; import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET; import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER; @@ -31,6 +32,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller.SessionInfo; +import android.os.Process; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -38,6 +40,8 @@ import android.view.View.OnClickListener; import android.widget.Toast; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.BuildConfig; +import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; @@ -58,8 +62,8 @@ import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.views.FloatingIconView; import com.android.launcher3.views.Snackbar; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; @@ -146,7 +150,28 @@ public class ItemClickHandler { private static void onClickAppPairIcon(View v) { Launcher launcher = Launcher.getLauncher(v.getContext()); AppPairIcon appPairIcon = (AppPairIcon) v; - launcher.launchAppPair(appPairIcon); + if (!appPairIcon.isLaunchableAtScreenSize()) { + // Display a message for app pairs that are disabled due to screen size + boolean isFoldable = InvariantDeviceProfile.INSTANCE.get(launcher) + .supportedProfiles.stream().anyMatch(dp -> dp.isTwoPanels); + Toast.makeText(launcher, isFoldable + ? R.string.app_pair_needs_unfold + : R.string.app_pair_unlaunchable_at_screen_size, + Toast.LENGTH_SHORT).show(); + } else if (appPairIcon.getInfo().isDisabled()) { + WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0); + WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1); + // Show the user why the app pair is disabled. + if (app1.isDisabled() && !handleDisabledItemClicked(app1, launcher)) { + // If handleDisabledItemClicked() did not handle the error message, we initiate an + // app launch so Framework can tell the user why the app is suspended. + onClickAppShortcut(v, app1, launcher); + } else if (app2.isDisabled() && !handleDisabledItemClicked(app2, launcher)) { + onClickAppShortcut(v, app2, launcher); + } + } else { + launcher.launchAppPair(appPairIcon); + } } /** @@ -187,16 +212,12 @@ public class ItemClickHandler { boolean downloadStarted) { ItemInfo item = (ItemInfo) v.getTag(); CompletableFuture siFuture; - if (Utilities.ATLEAST_Q) { - siFuture = CompletableFuture.supplyAsync(() -> - InstallSessionHelper.INSTANCE.get(launcher) - .getActiveSessionInfo(item.user, packageName), - UI_HELPER_EXECUTOR); - } else { - siFuture = CompletableFuture.completedFuture(null); - } + siFuture = CompletableFuture.supplyAsync(() -> + InstallSessionHelper.INSTANCE.get(launcher) + .getActiveSessionInfo(item.user, packageName), + UI_HELPER_EXECUTOR); Consumer marketLaunchAction = sessionInfo -> { - if (sessionInfo != null && Utilities.ATLEAST_Q) { + if (sessionInfo != null) { LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class); try { launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null, @@ -207,7 +228,8 @@ public class ItemClickHandler { } } // Fallback to using custom market intent. - Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName); + Intent intent = ApiWrapper.getAppMarketActivityIntent(launcher, + packageName, Process.myUserHandle()); launcher.startActivitySafely(v, intent, item); }; @@ -317,7 +339,8 @@ public class ItemClickHandler { } // Check for abandoned promise - if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) { + if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi() + && (!Utilities.enableSupportForArchiving() || !shortcut.isArchived())) { String packageName = shortcut.getIntent().getComponent() != null ? shortcut.getIntent().getComponent().getPackageName() : shortcut.getIntent().getPackage(); @@ -339,15 +362,21 @@ public class ItemClickHandler { private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity"); - Intent intent; - if (item instanceof ItemInfoWithIcon - && (((ItemInfoWithIcon) item).runtimeStatusFlags - & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { - ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item; - intent = new PackageManagerHelper(launcher) - .getMarketIntent(appInfo.getTargetComponent().getPackageName()); - } else { - intent = item.getIntent(); + Intent intent = item.getIntent(); + if (item instanceof ItemInfoWithIcon itemInfoWithIcon) { + if ((itemInfoWithIcon.runtimeStatusFlags + & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { + intent = ApiWrapper.getAppMarketActivityIntent(launcher, + itemInfoWithIcon.getTargetComponent().getPackageName(), + Process.myUserHandle()); + } else if ((itemInfoWithIcon.runtimeStatusFlags + & ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP) != 0) { + intent = ApiWrapper.getAppMarketActivityIntent(launcher, + BuildConfig.APPLICATION_ID, + launcher.getAppsView().getPrivateProfileManager().getProfileUser()); + launcher.getStatsLogManager().logger().log( + LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP); + } } if (intent == null) { throw new IllegalArgumentException("Input must have a valid intent"); diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java index 9e7d4dcb6e..116f13a89f 100644 --- a/src/com/android/launcher3/touch/ItemLongClickListener.java +++ b/src/com/android/launcher3/touch/ItemLongClickListener.java @@ -184,7 +184,7 @@ public class ItemLongClickListener { // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts) if (launcher.getDragController().isDragging()) return false; // Return early if user is in the middle of selecting split-screen apps - if (FeatureFlags.enableSplitContextually() && launcher.isSplitSelectionEnabled()) { + if (FeatureFlags.enableSplitContextually() && launcher.isSplitSelectionActive()) { return false; } diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java index 74d88ba922..e0c4e3c06b 100644 --- a/src/com/android/launcher3/touch/PagedOrientationHandler.java +++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java @@ -19,26 +19,11 @@ package com.android.launcher3.touch; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.ShapeDrawable; -import android.util.FloatProperty; -import android.util.Pair; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.accessibility.AccessibilityEvent; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.util.SplitConfigurationOptions; -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; -import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; - -import java.util.List; /** * Abstraction layer to separate horizontal and vertical specific implementations @@ -47,9 +32,7 @@ import java.util.List; */ public interface PagedOrientationHandler { - PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler(); - PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler(); - PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler(); + PagedOrientationHandler DEFAULT = new DefaultPagedViewHandler(); interface Int2DAction { void call(T target, int x, int y); @@ -64,39 +47,18 @@ public interface PagedOrientationHandler { void setPrimary(T target, Int2DAction action, int param); void setPrimary(T target, Float2DAction action, float param); - void setSecondary(T target, Float2DAction action, float param); - void set(T target, Int2DAction action, int primaryParam, int secondaryParam); float getPrimaryDirection(MotionEvent event, int pointerIndex); float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId); int getMeasuredSize(View view); - int getPrimarySize(View view); - float getPrimarySize(RectF rect); - float getStart(RectF rect); - float getEnd(RectF rect); - int getClearAllSidePadding(View view, boolean isRtl); - int getSecondaryDimension(View view); - FloatProperty getPrimaryViewTranslate(); - FloatProperty getSecondaryViewTranslate(); - int getPrimaryScroll(View view); float getPrimaryScale(View view); int getChildStart(View view); int getCenterForPage(View view, Rect insets); int getScrollOffsetStart(View view, Rect insets); int getScrollOffsetEnd(View view, Rect insets); - int getSecondaryTranslationDirectionFactor(); - int getSplitTranslationDirectionFactor(@StagePosition int stagePosition, - DeviceProfile deviceProfile); ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild); void setMaxScroll(AccessibilityEvent event, int maxScroll); boolean getRecentsRtlSetting(Resources resources); - float getDegreesRotated(); - int getRotation(); - void setPrimaryScale(View view, float scale); - void setSecondaryScale(View view, float scale); - - T getPrimaryValue(T x, T y); - T getSecondaryValue(T x, T y); int getPrimaryValue(int x, int y); int getSecondaryValue(int x, int y); @@ -104,174 +66,6 @@ public interface PagedOrientationHandler { float getPrimaryValue(float x, float y); float getSecondaryValue(float x, float y); - boolean isLayoutNaturalToLauncher(); - Pair getSplitSelectTaskOffset(FloatProperty primary, - FloatProperty secondary, DeviceProfile deviceProfile); - int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect); - List getSplitPositionOptions(DeviceProfile dp); - /** - * @param placeholderHeight height of placeholder view in portrait, width in landscape - */ - 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 updateSplitIconParams(View out, float onScreenRectCenterX, - float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, - int drawableWidth, int drawableHeight, DeviceProfile dp, - @StagePosition int stagePosition); - - /** - * Sets positioning and rotation for a SplitInstructionsView. - * @param out The SplitInstructionsView that needs to be positioned. - * @param dp The device profile, used to report rotation and device type. - * @param splitInstructionsHeight The SplitInstructionView's height. - * @param splitInstructionsWidth The SplitInstructionView's width. - */ - void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, - int splitInstructionsWidth); - - /** - * @param splitDividerSize height of split screen drag handle in portrait, width in landscape - * @param stagePosition the split position option (top/left, bottom/right) of the first - * task selected for entering split - * @param out1 the bounds for where the first selected app will be - * @param out2 the bounds for where the second selected app will be, complimentary to - * {@param out1} based on {@param initialSplitOption} - */ - void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, - @StagePosition int stagePosition, Rect out1, Rect out2); - - int getDefaultSplitPosition(DeviceProfile deviceProfile); - - /** - * @param outRect This is expected to be the rect that has the dimensions for a non-split, - * fullscreen task in overview. This will directly be modified. - * @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize - * outRect for - */ - void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, - @SplitConfigurationOptions.StagePosition int desiredStagePosition); - - void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, - int parentWidth, int parentHeight, - SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl); - - // Overview TaskMenuView methods - void setTaskIconParams(FrameLayout.LayoutParams iconParams, - int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl); - void setIconAppChipMenuParams(View iconAppChipMenuView, FrameLayout.LayoutParams iconMenuParams, - int iconMenuMargin, int thumbnailTopMargin); - void setSplitIconParams(View primaryIconView, View secondaryIconView, - int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, - DeviceProfile deviceProfile, SplitBounds splitConfig); - - /* - * The following two methods try to center the TaskMenuView in landscape by finding the center - * of the thumbnail view and then subtracting half of the taskMenu width. In this case, the - * taskMenu width is the same size as the thumbnail width (what got set below in - * getTaskMenuWidth()), so we directly use that in the calculations. - */ - float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile, - float taskInsetMargin, View taskViewIcon); - float getTaskMenuY(float y, View thumbnailView, int stagePosition, - View taskMenuView, float taskInsetMargin, View taskViewIcon); - int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, - @StagePosition int stagePosition); - /** - * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items - * inside task menu view. - */ - void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, - LinearLayout taskMenuLayout, int dividerSpacing, - ShapeDrawable dividerDrawable); - /** - * Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child - * views inside task menu view. - */ - void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, - LinearLayout viewGroup, DeviceProfile deviceProfile); - - /** - * 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, SplitBounds splitBounds, DeviceProfile deviceProfile, - View[] thumbnailViews, int desiredTaskId, View banner); - - // The following are only used by TaskViewTouchHandler. - /** @return Either VERTICAL or HORIZONTAL. */ - SingleAxisSwipeDetector.Direction getUpDownSwipeDirection(); - /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */ - int getUpDirection(boolean isRtl); - /** @return Whether the displacement is going towards the top of the screen. */ - boolean isGoingUp(float displacement, boolean isRtl); - /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */ - int getTaskDragDisplacementFactor(boolean isRtl); - - /** - * Maps the velocity from the coordinate plane of the foreground app to that - * of Launcher's (which now will always be portrait) - */ - 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); - - /** - * Determine the target translation for animating the FloatingTaskView out. This value could - * either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was - * docked. - * - * @param floatingTask The FloatingTaskView. - * @param onScreenRect The current on-screen dimensions of the FloatingTaskView. - * @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT. - * @param dp The device profile. - * @return A float. When an animation translates the FloatingTaskView to this position, it will - * appear to tuck away off the edge of the screen. - */ - float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, - @StagePosition int stagePosition, DeviceProfile dp); - - /** - * Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be - * either x or y), depending on how the view is oriented. - * - * @param floatingTask The FloatingTaskView to be translated. - * @param translation The target translation value. - * @param dp The current device profile. - */ - void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp); - - /** - * Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be - * either x or y), depending on how the view is oriented. - * - * @param floatingTask The FloatingTaskView in question. - * @param dp The current device profile. - * @return The current translation value. - */ - Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp); - class ChildBounds { public final int primaryDimension; @@ -279,8 +73,8 @@ public interface PagedOrientationHandler { public final int childPrimaryEnd; public final int childSecondaryEnd; - ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd, - int childSecondaryEnd) { + public ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd, + int childSecondaryEnd) { this.primaryDimension = primaryDimension; this.secondaryDimension = secondaryDimension; this.childPrimaryEnd = childPrimaryEnd; diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java index 5b6c9e0763..0ff10c26a5 100644 --- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java +++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java @@ -24,6 +24,7 @@ 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_SPLIT_SELECTION_EXIT_INTERRUPTED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_LONGPRESS; import android.graphics.PointF; @@ -206,8 +207,8 @@ public class WorkspaceTouchListener extends GestureDetector.SimpleOnGestureListe HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); mLauncher.getStatsLogManager().logger().log(LAUNCHER_WORKSPACE_LONGPRESS); mLauncher.showDefaultOptions(mTouchDownPoint.x, mTouchDownPoint.y); - if (FeatureFlags.enableSplitContextually() && mLauncher.isSplitSelectionEnabled()) { - mLauncher.dismissSplitSelection(); + if (FeatureFlags.enableSplitContextually() && mLauncher.isSplitSelectionActive()) { + mLauncher.dismissSplitSelection(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED); } } else { cancelLongPress(); diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java index 7af1a13d67..405d2bbdc6 100644 --- a/src/com/android/launcher3/util/ActivityTracker.java +++ b/src/com/android/launcher3/util/ActivityTracker.java @@ -15,13 +15,14 @@ */ package com.android.launcher3.util; +import static com.android.launcher3.testing.shared.TestProtocol.GET_FROM_RECENTS_FAILURE; +import static com.android.launcher3.testing.shared.TestProtocol.testLogD; + import androidx.annotation.Nullable; import com.android.launcher3.BaseActivity; import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashSet; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -40,6 +41,9 @@ public final class ActivityTracker { public void onActivityDestroyed(T activity) { if (mCurrentActivity.get() == activity) { + testLogD(GET_FROM_RECENTS_FAILURE, + String.format("ActivityTracker.onActivityDestroyed this=%s, activity=%s", + this, activity)); mCurrentActivity.clear(); } } @@ -71,6 +75,8 @@ public final class ActivityTracker { } public boolean handleCreate(T activity) { + testLogD(GET_FROM_RECENTS_FAILURE, + String.format("ActivityTracker.handleCreate this=%s, activity=%s", this, activity)); mCurrentActivity = new WeakReference<>(activity); return handleIntent(activity, false /* alreadyOnHome */); } diff --git a/src/com/android/launcher3/util/CancellableTask.kt b/src/com/android/launcher3/util/CancellableTask.kt new file mode 100644 index 0000000000..49ef020ff0 --- /dev/null +++ b/src/com/android/launcher3/util/CancellableTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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 java.util.concurrent.Executor +import java.util.function.Consumer +import java.util.function.Supplier + +/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */ +class CancellableTask +@JvmOverloads +constructor( + private val task: Supplier, + // Executor where consumer needs to be executed on. Typically UI executor. + private val callbackExecutor: Executor, + // Consumer that needs to be accepted upon completion of the task. Typically work that needs to + // be done in UI thread after task completes. + private val callback: Consumer, + // Callback to be executed on callbackExecutor at the end irrespective of the task being + // completed or cancelled + private val endRunnable: Runnable = Runnable {} +) : Runnable { + + // flag to cancel the callback + var canceled = false + private set + + private var ended = false + + override fun run() { + if (canceled) return + val value = task.get() + callbackExecutor.execute { + if (!canceled) { + callback.accept(value) + } + onEnd() + } + } + + /** + * Cancel the [CancellableTask] if not scheduled. If [CancellableTask] has started execution at + * this time, we will try to cancel the callback if not executed yet. + */ + fun cancel() { + canceled = true + callbackExecutor.execute(this::onEnd) + } + + private fun onEnd() { + if (!ended) { + ended = true + endRunnable.run() + } + } +} diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt index 18f833978a..85f81f57ba 100644 --- a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt +++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt @@ -26,6 +26,8 @@ import com.android.launcher3.anim.AnimatorListeners import com.android.launcher3.anim.AnimatorPlaybackController import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY +import com.android.launcher3.states.StateAnimationConfig.USER_CONTROLLED import java.util.function.Consumer private const val TAG = "CannedAnimCoordinator" @@ -107,8 +109,15 @@ class CannedAnimationCoordinator(private val activity: StatefulActivity<*>) { } // Link this to the state manager so that it auto-cancels when state changes recreatePending = false + // Animator coordinator takes care of reapplying the animation due to state reset. Set the + // flags accordingly animationController = - controller.apply { activity.stateManager.setCurrentUserControlledAnimation(this) } + controller.apply { + activity.stateManager.setCurrentAnimation( + this, + USER_CONTROLLED or HANDLE_STATE_APPLY + ) + } recreateAnimation(provider) } diff --git a/src/com/android/launcher3/util/CellContentDimensions.kt b/src/com/android/launcher3/util/CellContentDimensions.kt index 3c8e0c44ca..5059c2f98e 100644 --- a/src/com/android/launcher3/util/CellContentDimensions.kt +++ b/src/com/android/launcher3/util/CellContentDimensions.kt @@ -48,7 +48,10 @@ class CellContentDimensions( cellContentHeight = getCellContentHeight() // Step 3. Decrease label size - if (cellContentHeight > cellHeightPx) { + if ( + cellContentHeight > cellHeightPx && + iconTextSizePx > iconSizeSteps.minimumIconLabelSize + ) { iconTextSizePx = max( iconSizeSteps.minimumIconLabelSize, @@ -58,6 +61,17 @@ class CellContentDimensions( } } + // For some cases, depending on the display size, the content might not fit inside the + // cell height after considering the minimum icon and label size allowed. + // For these extreme cases, we will allow the icon size to be smaller than + // [IconSizeSteps.minimumIconSize] to fit inside the cell height without cropping. + while ( + cellContentHeight > cellHeightPx && iconSizePx > IconSizeSteps.ICON_SIZE_STEP_EXTRA + ) { + iconSizePx -= IconSizeSteps.ICON_SIZE_STEP_EXTRA + cellContentHeight = getCellContentHeight() + } + return cellContentHeight } diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java index 18f583d702..ff9521282d 100644 --- a/src/com/android/launcher3/util/DisplayController.java +++ b/src/com/android/launcher3/util/DisplayController.java @@ -175,6 +175,13 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { sTransientTaskbarStatusForTests = enable; } + /** + * Returns whether the taskbar is pinned in gesture navigation mode. + */ + public static boolean isPinnedTaskbar(Context context) { + return INSTANCE.get(context).getInfo().isPinnedTaskbar(); + } + @Override public void close() { mDestroyed = true; @@ -355,10 +362,10 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { WindowManagerProxy wmProxy, Map> perDisplayBoundsCache) { CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext); - normalizedDisplayInfo = displayInfo.normalize(); + normalizedDisplayInfo = displayInfo.normalize(wmProxy); rotation = displayInfo.rotation; currentSize = displayInfo.size; - cutout = displayInfo.cutout; + cutout = WindowManagerProxy.getSafeInsets(displayInfo.cutout); Configuration config = displayInfoContext.getResources().getConfiguration(); fontScale = config.fontScale; @@ -423,6 +430,12 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable { } return true; } + /** + * Returns whether the taskbar is pinned in gesture navigation mode. + */ + public boolean isPinnedTaskbar() { + return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar(); + } /** * Returns {@code true} if the bounds represent a tablet. diff --git a/src/com/android/launcher3/util/EdgeEffectCompat.java b/src/com/android/launcher3/util/EdgeEffectCompat.java index 491582b87f..ca3725923e 100644 --- a/src/com/android/launcher3/util/EdgeEffectCompat.java +++ b/src/com/android/launcher3/util/EdgeEffectCompat.java @@ -16,6 +16,7 @@ package com.android.launcher3.util; import android.content.Context; +import android.view.MotionEvent; import android.widget.EdgeEffect; import com.android.launcher3.Utilities; @@ -43,4 +44,14 @@ public class EdgeEffectCompat extends EdgeEffect { return deltaDistance; } } + + public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) { + return onPullDistance(deltaDistance, displacement); + } + + public void onFlingVelocity(int velocity) { } + + public void onRelease(MotionEvent ev) { + onRelease(); + } } diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java index 1cec0ecc10..296efe9277 100644 --- a/src/com/android/launcher3/util/IOUtils.java +++ b/src/com/android/launcher3/util/IOUtils.java @@ -19,7 +19,6 @@ package com.android.launcher3.util; import android.os.FileUtils; import android.util.Log; -import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import java.io.ByteArrayOutputStream; @@ -51,17 +50,7 @@ public class IOUtils { } public static long copy(InputStream from, OutputStream to) throws IOException { - if (Utilities.ATLEAST_Q) { - return FileUtils.copy(from, to); - } - byte[] buf = new byte[BUF_SIZE]; - long total = 0; - int r; - while ((r = from.read(buf)) != -1) { - to.write(buf, 0, r); - total += r; - } - return total; + return FileUtils.copy(from, to); } public static void closeSilently(Closeable c) { diff --git a/src/com/android/launcher3/util/IconSizeSteps.kt b/src/com/android/launcher3/util/IconSizeSteps.kt index 6128eb4dfb..a207d5c6bd 100644 --- a/src/com/android/launcher3/util/IconSizeSteps.kt +++ b/src/com/android/launcher3/util/IconSizeSteps.kt @@ -49,5 +49,9 @@ class IconSizeSteps(res: Resources) { companion object { internal const val TEXT_STEP = 1 + + // This icon extra step is used for stepping down logic in extreme cases when it's + // necessary to reduce the icon size below minimum size available in [icon_size_steps]. + internal const val ICON_SIZE_STEP_EXTRA = 2 } } diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt new file mode 100644 index 0000000000..cc66af1189 --- /dev/null +++ b/src/com/android/launcher3/util/ItemInflater.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 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.appwidget.AppWidgetHostView +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import com.android.launcher3.BubbleTextView +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.R +import com.android.launcher3.apppairs.AppPairIcon +import com.android.launcher3.folder.FolderIcon +import com.android.launcher3.model.ModelWriter +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.views.ActivityContext +import com.android.launcher3.widget.LauncherWidgetHolder +import com.android.launcher3.widget.PendingAppWidgetHostView +import com.android.launcher3.widget.WidgetInflater + +/** Utility class to inflate View for a model item */ +class ItemInflater( + private val context: T, + private val widgetHolder: LauncherWidgetHolder, + private val clickListener: OnClickListener, + private val focusListener: OnFocusChangeListener, + private val defaultParent: ViewGroup +) where T : Context, T : ActivityContext { + + private val widgetInflater = WidgetInflater(context) + + @JvmOverloads + fun inflateItem(item: ItemInfo, writer: ModelWriter, nullableParent: ViewGroup? = null): View? { + val parent = nullableParent ?: defaultParent + when (item.itemType) { + Favorites.ITEM_TYPE_APPLICATION, + Favorites.ITEM_TYPE_DEEP_SHORTCUT, + Favorites.ITEM_TYPE_SEARCH_ACTION -> { + var info = + if (item is WorkspaceItemFactory) { + (item as WorkspaceItemFactory).makeWorkspaceItem(context) + } else { + item as WorkspaceItemInfo + } + if (info.container == Favorites.CONTAINER_PREDICTION) { + // Came from all apps prediction row -- make a copy + info = WorkspaceItemInfo(info) + } + return createShortcut(info, parent) + } + Favorites.ITEM_TYPE_FOLDER -> + return FolderIcon.inflateFolderAndIcon( + R.layout.folder_icon, + context, + parent, + item as FolderInfo + ) + Favorites.ITEM_TYPE_APP_PAIR -> + return AppPairIcon.inflateIcon( + R.layout.app_pair_icon, + context, + parent, + item as FolderInfo + ) + Favorites.ITEM_TYPE_APPWIDGET, + Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> + return inflateAppWidget(item as LauncherAppWidgetInfo, writer) + else -> throw RuntimeException("Invalid Item Type") + } + } + + /** + * Creates a view representing a shortcut inflated from the specified resource. + * + * @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. + */ + private fun createShortcut(info: WorkspaceItemInfo, parent: ViewGroup): View { + val favorite = + LayoutInflater.from(parent.context).inflate(R.layout.app_icon, parent, false) + as BubbleTextView + favorite.applyFromWorkspaceItem(info) + favorite.setOnClickListener(clickListener) + favorite.onFocusChangeListener = focusListener + return favorite + } + + private fun inflateAppWidget(item: LauncherAppWidgetInfo, writer: ModelWriter): View? { + TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId) + try { + val (type, reason, _, isUpdate, widgetInfo) = widgetInflater.inflateAppWidget(item) + if (type == WidgetInflater.TYPE_DELETE) { + writer.deleteItemFromDatabase(item, reason) + return null + } + if (isUpdate) { + writer.updateItemInDatabase(item) + } + val view = + if (type == WidgetInflater.TYPE_PENDING || widgetInfo == null) + PendingAppWidgetHostView(context, widgetHolder, item, widgetInfo) + else widgetHolder.createView(item.appWidgetId, widgetInfo) + prepareAppWidget(view, item) + return view + } finally { + TraceHelper.INSTANCE.endSection() + } + } + + fun prepareAppWidget(hostView: AppWidgetHostView, item: LauncherAppWidgetInfo) { + hostView.tag = item + hostView.isFocusable = true + hostView.onFocusChangeListener = focusListener + } +} diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index b6af3140fa..307411151e 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -69,6 +69,15 @@ public abstract class ItemInfoMatcher { .anyMatch(childOperator); } + /** + * Returns a matcher for items within app pairs. + */ + public static Predicate forAppPairMatch(Predicate childOperator) { + Predicate isAppPair = info -> + info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR; + return isAppPair.and(forFolderMatch(childOperator)); + } + /** * Returns a matcher for items with provided ids */ diff --git a/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java index c9db83d97d..e4e0bae06d 100644 --- a/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java +++ b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java @@ -17,6 +17,7 @@ package com.android.launcher3.util; 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.accessibility.LauncherAccessibilityDelegate.getSupportedActions; import android.util.Log; @@ -27,6 +28,7 @@ import android.view.Menu; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.accessibility.BaseAccessibilityDelegate; @@ -118,17 +120,22 @@ public class KeyboardShortcutsDelegate { return true; } else if (mLauncher.getAppsView().isInAllApps()) { // Close all apps if there are no open floating views. - closeAllApps(); + mLauncher.getStateManager().goToState(NORMAL, true); + return true; + } else if (mLauncher.isInState(LauncherState.OVERVIEW) + || mLauncher.isInState(LauncherState.OVERVIEW_SPLIT_SELECT)) { + // Close Overview and return to home. + mLauncher.getStateManager().goToState(NORMAL, true); + return true; + } else if (mLauncher.isInState(LauncherState.OVERVIEW_MODAL_TASK)) { + // Return to the previous state (Overview) when the modal task is open. + mLauncher.getStateManager().goToState(OVERVIEW, true); return true; } } return null; } - private void closeAllApps() { - mLauncher.getStateManager().goToState(NORMAL, true); - } - /** * Handle key up event. * @param keyCode code of the key being pressed. diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java index f73940bfee..69786bb5e5 100644 --- a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java +++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java @@ -19,6 +19,7 @@ import android.graphics.drawable.Drawable; import android.view.View; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.PreloadIconDrawable; @@ -58,6 +59,8 @@ public interface LauncherBindableItemsContainer { : null); } else if (info instanceof FolderInfo && v instanceof FolderIcon) { ((FolderIcon) v).updatePreviewItems(updates::contains); + } else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) { + appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); } // Iterate all items @@ -86,6 +89,8 @@ public interface LauncherBindableItemsContainer { ((PendingAppWidgetHostView) v).applyState(); } else if (v instanceof FolderIcon && info instanceof FolderInfo) { ((FolderIcon) v).updatePreviewItems(updates::contains); + } else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) { + appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); } // process all the shortcuts return false; diff --git a/src/com/android/launcher3/util/OverlayEdgeEffect.java b/src/com/android/launcher3/util/OverlayEdgeEffect.java index 2ef1e1ff43..d09d801d66 100644 --- a/src/com/android/launcher3/util/OverlayEdgeEffect.java +++ b/src/com/android/launcher3/util/OverlayEdgeEffect.java @@ -17,10 +17,13 @@ package com.android.launcher3.util; import android.content.Context; import android.graphics.Canvas; +import android.os.SystemClock; +import android.view.MotionEvent; import android.widget.EdgeEffect; +import com.android.launcher3.BuildConfig; import com.android.launcher3.Utilities; -import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay; +import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy; /** * Extension of {@link EdgeEffect} which shows the Launcher overlay @@ -28,11 +31,11 @@ import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverla public class OverlayEdgeEffect extends EdgeEffectCompat { protected float mDistance; - protected final LauncherOverlay mOverlay; + protected final LauncherOverlayTouchProxy mOverlay; protected boolean mIsScrolling; protected final boolean mIsRtl; - public OverlayEdgeEffect(Context context, LauncherOverlay overlay) { + public OverlayEdgeEffect(Context context, LauncherOverlayTouchProxy overlay) { super(context); mOverlay = overlay; mIsRtl = Utilities.isRtl(context.getResources()); @@ -44,12 +47,30 @@ public class OverlayEdgeEffect extends EdgeEffectCompat { } public float onPullDistance(float deltaDistance, float displacement) { + // Fallback implementation, will never actually get called + if (BuildConfig.IS_DEBUG_DEVICE) { + throw new RuntimeException("Wrong method called"); + } + MotionEvent mv = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, displacement, 0, 0); + try { + return onPullDistance(deltaDistance, displacement, mv); + } finally { + mv.recycle(); + } + } + + @Override + public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) { mDistance = Math.max(0f, deltaDistance + mDistance); if (!mIsScrolling) { - mOverlay.onScrollInteractionBegin(); + int originalAction = ev.getAction(); + ev.setAction(MotionEvent.ACTION_DOWN); + mOverlay.onOverlayMotionEvent(ev, 0); + ev.setAction(originalAction); mIsScrolling = true; } - mOverlay.onScrollChange(mDistance, mIsRtl); + mOverlay.onOverlayMotionEvent(ev, mDistance); return mDistance > 0 ? deltaDistance : 0; } @@ -63,9 +84,30 @@ public class OverlayEdgeEffect extends EdgeEffectCompat { @Override public void onRelease() { + // Fallback implementation, will never actually get called + if (BuildConfig.IS_DEBUG_DEVICE) { + throw new RuntimeException("Wrong method called"); + } + MotionEvent mv = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, mDistance, 0, 0); + onRelease(mv); + mv.recycle(); + } + + @Override + public void onFlingVelocity(int velocity) { + mOverlay.onFlingVelocity(velocity); + } + + @Override + public void onRelease(MotionEvent ev) { if (mIsScrolling) { + int originalAction = ev.getAction(); + ev.setAction(MotionEvent.ACTION_UP); + mOverlay.onOverlayMotionEvent(ev, mDistance); + ev.setAction(originalAction); + mDistance = 0; - mOverlay.onScrollInteractionEnd(); mIsScrolling = false; } } diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index 91203a7f9b..11d8e970f0 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -28,8 +28,8 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.graphics.Rect; -import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -46,6 +46,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.uioverrides.ApiWrapper; import java.net.URISyntaxException; import java.util.List; @@ -103,6 +104,23 @@ public class PackageManagerHelper { return info != null; } + /** + * Returns whether the target app is in archived state + */ + @SuppressWarnings("NewApi") + public boolean isAppArchived(@NonNull final String packageName) { + final ApplicationInfo info; + try { + info = mPm.getPackageInfo(packageName, + PackageManager.PackageInfoFlags.of( + PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo; + return info.isArchived; + } catch (NameNotFoundException e) { + Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e); + return false; + } + } + /** * Returns the application info for the provided package or null */ @@ -111,17 +129,12 @@ public class PackageManagerHelper { @NonNull final UserHandle user, final int flags) { try { ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user); - return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled - ? null : info; + return !isPackageInstalledOrArchived(info) || !info.enabled ? null : info; } catch (PackageManager.NameNotFoundException e) { return null; } } - public boolean isSafeMode() { - return mPm.isSafeMode(); - } - @Nullable public Intent getAppLaunchIntent(@Nullable final String pkg, @NonNull final UserHandle user) { List activities = mLauncherApps.getActivityList(pkg, user); @@ -137,17 +150,6 @@ public class PackageManagerHelper { return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; } - public Intent getMarketIntent(String packageName) { - return new Intent(Intent.ACTION_VIEW) - .setData(new Uri.Builder() - .scheme("market") - .authority("details") - .appendQueryParameter("id", packageName) - .build()) - .putExtra(Intent.EXTRA_REFERRER, new Uri.Builder().scheme("android-app") - .authority(mContext.getPackageName()).build()); - } - /** * Creates a new market search intent. */ @@ -172,8 +174,8 @@ public class PackageManagerHelper { && (((ItemInfoWithIcon) info).runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { ItemInfoWithIcon appInfo = (ItemInfoWithIcon) info; - mContext.startActivity(new PackageManagerHelper(mContext) - .getMarketIntent(appInfo.getTargetComponent().getPackageName())); + mContext.startActivity(ApiWrapper.getAppMarketActivityIntent(mContext, + appInfo.getTargetComponent().getPackageName(), Process.myUserHandle())); return; } ComponentName componentName = null; @@ -267,4 +269,11 @@ public class PackageManagerHelper { } return 100; } + + /** Returns true in case app is installed on the device or in archived state. */ + @SuppressWarnings("NewApi") + private boolean isPackageInstalledOrArchived(ApplicationInfo info) { + return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || ( + Utilities.enableSupportForArchiving() && info.isArchived); + } } diff --git a/src/com/android/launcher3/util/RunnableList.java b/src/com/android/launcher3/util/RunnableList.java index f6e0c57fb2..2b8bf56a3b 100644 --- a/src/com/android/launcher3/util/RunnableList.java +++ b/src/com/android/launcher3/util/RunnableList.java @@ -69,4 +69,11 @@ public class RunnableList { } } } + + /** + * Returns true if the list has been destroyed + */ + public boolean isDestroyed() { + return mDestroyed; + } } diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java index f4a022544c..837d7bc04d 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -169,6 +169,15 @@ public final class SplitConfigurationOptions { dividerWidthPercent = visualDividerBounds.width() / totalWidth; dividerHeightPercent = visualDividerBounds.height() / totalHeight; } + + @Override + public String toString() { + return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n" + + "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId + "\n" + + "Divider: " + visualDividerBounds + "\n" + + "AppsVertical? " + appsStackedVertically + "\n" + + "snapPosition: " + snapPosition; + } } public static class SplitStageInfo { diff --git a/src/com/android/launcher3/util/StartActivityParams.java b/src/com/android/launcher3/util/StartActivityParams.java index b48562f2d8..d66b0a048a 100644 --- a/src/com/android/launcher3/util/StartActivityParams.java +++ b/src/com/android/launcher3/util/StartActivityParams.java @@ -52,6 +52,7 @@ public class StartActivityParams implements Parcelable { public int flagsValues; public int extraFlags; public Bundle options; + public boolean requireActivityResult = true; public StartActivityParams(Activity activity, int requestCode) { this(activity.createPendingResult(requestCode, new Intent(), @@ -74,6 +75,7 @@ public class StartActivityParams implements Parcelable { flagsValues = parcel.readInt(); extraFlags = parcel.readInt(); options = parcel.readBundle(); + requireActivityResult = parcel.readInt() != 0; } @@ -94,6 +96,7 @@ public class StartActivityParams implements Parcelable { parcel.writeInt(flagsValues); parcel.writeInt(extraFlags); parcel.writeBundle(options); + parcel.writeInt(requireActivityResult ? 1 : 0); } /** Perform the operation on the pendingIntent. */ diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java index 138cc4af3e..edcd3f6059 100644 --- a/src/com/android/launcher3/util/TraceHelper.java +++ b/src/com/android/launcher3/util/TraceHelper.java @@ -20,12 +20,10 @@ import android.os.Trace; import androidx.annotation.MainThread; -import com.android.launcher3.Utilities; +import kotlin.random.Random; import java.util.function.Supplier; -import kotlin.random.Random; - /** * A wrapper around {@link Trace} to allow better testing. * @@ -67,9 +65,6 @@ public class TraceHelper { @SuppressWarnings("NewApi") @SuppressLint("NewApi") public SafeCloseable beginAsyncSection(String sectionName) { - if (!Utilities.ATLEAST_Q) { - return () -> { }; - } int cookie = Random.Default.nextInt(); Trace.beginAsyncSection(sectionName, cookie); return () -> Trace.endAsyncSection(sectionName, cookie); @@ -81,9 +76,6 @@ public class TraceHelper { @SuppressWarnings("NewApi") @SuppressLint("NewApi") public SafeCloseable allowIpcs(String rpcName) { - if (!Utilities.ATLEAST_Q) { - return () -> { }; - } int cookie = Random.Default.nextInt(); Trace.beginAsyncSection(rpcName, cookie); return () -> Trace.endAsyncSection(rpcName, cookie); diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java index 4f20bbcf44..e1695e96c3 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/src/com/android/launcher3/util/VibratorWrapper.java @@ -19,21 +19,19 @@ import static android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK; import static android.os.VibrationEffect.createPredefined; import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; -import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY; -import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT; -import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS; -import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT; -import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT; +import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_DELAY; +import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_END_SCALE_PERCENT; +import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_ITERATIONS; +import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_SCALE_EXPONENT; +import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_START_SCALE_PERCENT; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.media.AudioAttributes; -import android.os.Build; import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; @@ -41,14 +39,12 @@ import android.provider.Settings; import androidx.annotation.Nullable; -import com.android.launcher3.LauncherPrefs; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; /** * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. */ -@TargetApi(Build.VERSION_CODES.Q) public class VibratorWrapper { public static final MainThreadInitializedObject INSTANCE = @@ -138,7 +134,7 @@ public class VibratorWrapper { mThresholdUntilNextDragCallMillis = 0; } - if (Utilities.ATLEAST_R && mVibrator.areAllPrimitivesSupported( + if (mVibrator.areAllPrimitivesSupported( VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, VibrationEffect.Composition.PRIMITIVE_TICK)) { if (FeatureFlags.ENABLE_SEARCH_HAPTIC_HINT.get()) { @@ -226,8 +222,7 @@ public class VibratorWrapper { public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { if (mHasVibrator && mIsHapticFeedbackEnabled) { UI_HELPER_EXECUTOR.execute(() -> { - if (Utilities.ATLEAST_R && primitiveId >= 0 - && mVibrator.areAllPrimitivesSupported(primitiveId)) { + if (primitiveId >= 0 && mVibrator.areAllPrimitivesSupported(primitiveId)) { mVibrator.vibrate(VibrationEffect.startComposition() .addPrimitive(primitiveId, primitiveScale) .compose(), VIBRATION_ATTRS); @@ -261,17 +256,11 @@ public class VibratorWrapper { public void vibrateForSearchHint() { if (FeatureFlags.ENABLE_SEARCH_HAPTIC_HINT.get() && Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { - LauncherPrefs launcherPrefs = LauncherPrefs.get(mContext); - float startScale = launcherPrefs.get( - LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT) / 100f; - float endScale = launcherPrefs.get( - LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT) / 100f; - int scaleExponent = launcherPrefs.get( - LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT); - int iterations = launcherPrefs.get( - LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS); - int delayMs = launcherPrefs.get( - LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY); + float startScale = LPNH_HAPTIC_HINT_START_SCALE_PERCENT.get() / 100f; + float endScale = LPNH_HAPTIC_HINT_END_SCALE_PERCENT.get() / 100f; + int scaleExponent = LPNH_HAPTIC_HINT_SCALE_EXPONENT.get(); + int iterations = LPNH_HAPTIC_HINT_ITERATIONS.get(); + int delayMs = LPNH_HAPTIC_HINT_DELAY.get(); VibrationEffect.Composition composition = VibrationEffect.startComposition(); for (int i = 0; i < iterations; i++) { diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java index fada4a3f01..26bfd36dcf 100644 --- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java +++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java @@ -20,6 +20,8 @@ import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewTreeObserver.OnDrawListener; +import androidx.annotation.NonNull; + import com.android.launcher3.Launcher; import java.util.function.Consumer; @@ -31,26 +33,23 @@ public class ViewOnDrawExecutor implements OnDrawListener, Runnable, OnAttachStateChangeListener { private final RunnableList mTasks; - - private Consumer mOnClearCallback; + private final Consumer mOnClearCallback; private View mAttachedView; private boolean mCompleted; - private boolean mLoadAnimationCompleted; private boolean mFirstDrawCompleted; private boolean mCancelled; - public ViewOnDrawExecutor(RunnableList tasks) { + public ViewOnDrawExecutor(RunnableList tasks, + @NonNull Consumer onClearCallback) { mTasks = tasks; + mOnClearCallback = onClearCallback; } public void attachTo(Launcher launcher) { - mOnClearCallback = launcher::clearPendingExecutor; mAttachedView = launcher.getWorkspace(); - mAttachedView.addOnAttachStateChangeListener(this); - if (mAttachedView.isAttachedToWindow()) { attachObserver(); } @@ -77,17 +76,10 @@ public class ViewOnDrawExecutor implements OnDrawListener, Runnable, mAttachedView.post(this); } - public void onLoadAnimationCompleted() { - mLoadAnimationCompleted = true; - if (mAttachedView != null) { - mAttachedView.post(this); - } - } - @Override public void run() { - // Post the pending tasks after both onDraw and onLoadAnimationCompleted have been called. - if (mLoadAnimationCompleted && mFirstDrawCompleted && !mCompleted) { + // Post the pending tasks after first draw + if (mFirstDrawCompleted && !mCompleted) { markCompleted(); } } @@ -104,9 +96,8 @@ public class ViewOnDrawExecutor implements OnDrawListener, Runnable, mAttachedView.getViewTreeObserver().removeOnDrawListener(this); mAttachedView.removeOnAttachStateChangeListener(this); } - if (mOnClearCallback != null) { - mOnClearCallback.accept(this); - } + + mOnClearCallback.accept(this); } public void cancel() { diff --git a/src/com/android/launcher3/util/window/CachedDisplayInfo.java b/src/com/android/launcher3/util/window/CachedDisplayInfo.java index 23f37aa2b2..c5084ad7eb 100644 --- a/src/com/android/launcher3/util/window/CachedDisplayInfo.java +++ b/src/com/android/launcher3/util/window/CachedDisplayInfo.java @@ -16,13 +16,16 @@ 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.Insets; import android.graphics.Point; -import android.graphics.Rect; +import android.view.DisplayCutout; import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.Objects; /** @@ -30,36 +33,40 @@ import java.util.Objects; */ public class CachedDisplayInfo { + private static final DisplayCutout NO_CUTOUT = + new DisplayCutout(Insets.NONE, null, null, null, null); + public final Point size; public final int rotation; - public final Rect cutout; + @NonNull + public final DisplayCutout cutout; public CachedDisplayInfo() { this(new Point(0, 0), 0); } public CachedDisplayInfo(Point size, int rotation) { - this(size, rotation, new Rect()); + this(size, rotation, NO_CUTOUT); } - public CachedDisplayInfo(Point size, int rotation, Rect cutout) { + public CachedDisplayInfo(Point size, int rotation, @Nullable DisplayCutout cutout) { this.size = size; this.rotation = rotation; - this.cutout = cutout; + this.cutout = cutout == null ? NO_CUTOUT : cutout; } /** * Returns a CachedDisplayInfo where the properties are normalized to {@link Surface#ROTATION_0} */ - public CachedDisplayInfo normalize() { + public CachedDisplayInfo normalize(WindowManagerProxy windowManagerProxy) { 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)); + DisplayCutout newCutout = windowManagerProxy.rotateCutout( + cutout, size.x, size.y, rotation, Surface.ROTATION_0); return new CachedDisplayInfo(newSize, Surface.ROTATION_0, newCutout); } @@ -79,11 +86,16 @@ public class CachedDisplayInfo { CachedDisplayInfo that = (CachedDisplayInfo) o; return rotation == that.rotation && Objects.equals(size, that.size) - && Objects.equals(cutout, that.cutout); + && cutout.getSafeInsetLeft() == that.cutout.getSafeInsetLeft() + && cutout.getSafeInsetTop() == that.cutout.getSafeInsetTop() + && cutout.getSafeInsetRight() == that.cutout.getSafeInsetRight() + && cutout.getSafeInsetBottom() == that.cutout.getSafeInsetBottom(); } @Override public int hashCode() { - return Objects.hash(size, rotation, cutout); + return Objects.hash(size, rotation, + cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); } } diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java index 51a96c4d8a..4a906d33e1 100644 --- a/src/com/android/launcher3/util/window/WindowManagerProxy.java +++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java @@ -17,6 +17,7 @@ package com.android.launcher3.util.window; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.launcher3.Utilities.dpToPx; import static com.android.launcher3.Utilities.dpiFromPx; import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE; import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT; @@ -49,6 +50,9 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.testing.shared.ResourceUtils; @@ -95,7 +99,7 @@ public class WindowManagerProxy implements ResourceBasedOverride { */ public ArrayMap> estimateInternalDisplayBounds( Context displayInfoContext) { - CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(); + CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(this); List bounds = estimateWindowBounds(displayInfoContext, info); ArrayMap> result = new ArrayMap<>(); result.put(info, bounds); @@ -105,24 +109,7 @@ public class WindowManagerProxy implements ResourceBasedOverride { /** * Returns the real bounds for the provided display after applying any insets normalization */ - @TargetApi(Build.VERSION_CODES.R) public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) { - if (!Utilities.ATLEAST_R) { - Point smallestSize = new Point(); - Point largestSize = new Point(); - getDisplay(displayInfoContext).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 - return new WindowBounds(info.size.x, info.size.y, largestSize.x, smallestSize.y, - info.rotation); - } - } - WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) .getMaximumWindowMetrics(); Rect insets = new Rect(); @@ -133,10 +120,9 @@ public class WindowManagerProxy implements ResourceBasedOverride { /** * 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) { + if (!mTaskbarDrawnInProcess) { outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(), oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom()); return oldInsets; @@ -148,18 +134,29 @@ public class WindowManagerProxy implements ResourceBasedOverride { Resources systemRes = context.getResources(); Configuration config = systemRes.getConfiguration(); - boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH; + boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH; boolean isGesture = isGestureNav(context); boolean isPortrait = config.screenHeightDp > config.screenWidthDp; - int bottomNav = isTablet + int bottomNav = isLargeScreen ? 0 : (isPortrait ? getDimenByName(systemRes, NAVBAR_HEIGHT) : (isGesture ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0)); - Insets newNavInsets = Insets.of(navInsets.left, navInsets.top, navInsets.right, bottomNav); + int leftNav = navInsets.left; + int rightNav = navInsets.right; + if (!isLargeScreen && !isGesture && !isPortrait) { + // In 3-button landscape/seascape, Launcher should always have nav insets regardless if + // it's initiated from fullscreen apps. + int navBarWidth = getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); + switch (getRotation(context)) { + case Surface.ROTATION_90 -> rightNav = navBarWidth; + case Surface.ROTATION_270 -> leftNav = navBarWidth; + } + } + Insets newNavInsets = Insets.of(leftNav, navInsets.top, rightNav, bottomNav); insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets); @@ -183,6 +180,9 @@ public class WindowManagerProxy implements ResourceBasedOverride { insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); } + applyDisplayCutoutBottomInsetOverrideOnLargeScreen( + context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder); + WindowInsets result = insetsBuilder.build(); Insets systemWindowInsets = result.getInsetsIgnoringVisibility( WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); @@ -191,6 +191,71 @@ public class WindowManagerProxy implements ResourceBasedOverride { return result; } + /** + * For large screen, when display cutout is at bottom left/right corner of screen, override + * display cutout's bottom inset to 0, because launcher allows drawing content over that area. + */ + private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen( + @NonNull Context context, + boolean isLargeScreen, + int screenWidthPx, + @NonNull WindowInsets windowInsets, + @NonNull WindowInsets.Builder insetsBuilder) { + if (!isLargeScreen || !Utilities.ATLEAST_S) { + return; + } + + final DisplayCutout displayCutout = windowInsets.getDisplayCutout(); + if (displayCutout == null) { + return; + } + + if (!areBottomDisplayCutoutsSmallAndAtCorners( + displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) { + return; + } + + Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout()); + Insets newDisplayCutoutInset = Insets.of( + oldDisplayCutoutInset.left, + oldDisplayCutoutInset.top, + oldDisplayCutoutInset.right, + 0); + insetsBuilder.setInsetsIgnoringVisibility( + WindowInsets.Type.displayCutout(), newDisplayCutoutInset); + } + + /** + * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)} + */ + private static boolean areBottomDisplayCutoutsSmallAndAtCorners( + @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) { + return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx, + res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout)); + } + + /** + * Return true if bottom display cutouts are at bottom left/right corners, AND has width or + * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx + * passed to this method should be in the SAME screen rotation. + * + * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation + * @param screenWidthPx screen width in px based on current screen rotation + * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout. + */ + @VisibleForTesting + static boolean areBottomDisplayCutoutsSmallAndAtCorners( + @NonNull Rect cutoutRectBottom, int screenWidthPx, + int maxWidthAndHeightOfSmallCutoutPx) { + // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore + // it by returning false. + if (cutoutRectBottom.isEmpty()) { + return false; + } + return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx) + || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx); + } + protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) { Resources systemRes = context.getResources(); int statusBarHeight = getDimenByName(systemRes, @@ -204,10 +269,9 @@ public class WindowManagerProxy implements ResourceBasedOverride { * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations */ protected List estimateWindowBounds(Context context, - CachedDisplayInfo displayInfo) { + final CachedDisplayInfo displayInfo) { int densityDpi = context.getResources().getConfiguration().densityDpi; - int rotation = displayInfo.rotation; - Rect safeCutout = displayInfo.cutout; + final int rotation = displayInfo.rotation; int minSize = Math.min(displayInfo.size.x, displayInfo.size.y); int swDp = (int) dpiFromPx(minSize, densityDpi); @@ -220,8 +284,7 @@ public class WindowManagerProxy implements ResourceBasedOverride { } boolean isTablet = swDp >= MIN_TABLET_WIDTH; - boolean isTabletOrGesture = isTablet - || (Utilities.ATLEAST_R && isGestureNav(context)); + boolean isTabletOrGesture = isTablet || isGestureNav(context); // Use the status bar height resources because current system API to get the status bar // height doesn't allow to do this for an arbitrary display, it returns value only @@ -266,8 +329,15 @@ public class WindowManagerProxy implements ResourceBasedOverride { statusBarHeight = statusBarHeightLandscape; } - Rect insets = new Rect(safeCutout); - rotateRect(insets, rotationChange); + DisplayCutout rotatedCutout = rotateCutout( + displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i); + Rect insets = getSafeInsets(rotatedCutout); + if (areBottomDisplayCutoutsSmallAndAtCorners( + rotatedCutout.getBoundingRectBottom(), + bounds.width(), + context.getResources())) { + insets.bottom = 0; + } insets.top = Math.max(insets.top, statusBarHeight); insets.bottom = Math.max(insets.bottom, navBarHeight); @@ -317,8 +387,7 @@ public class WindowManagerProxy implements ResourceBasedOverride { Point size = new Point(); Display display = getDisplay(displayInfoContext); display.getRealSize(size); - Rect cutoutRect = new Rect(); - return new CachedDisplayInfo(size, rotation, cutoutRect); + return new CachedDisplayInfo(size, rotation); } } @@ -328,13 +397,8 @@ public class WindowManagerProxy implements ResourceBasedOverride { @TargetApi(Build.VERSION_CODES.S) protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) { Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom); - Rect cutoutRect = new Rect(); - DisplayCutout cutout = windowMetrics.getWindowInsets().getDisplayCutout(); - if (cutout != null) { - cutoutRect.set(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), - cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); - } - return new CachedDisplayInfo(size, rotation, cutoutRect); + return new CachedDisplayInfo(size, rotation, + windowMetrics.getWindowInsets().getDisplayCutout()); } /** @@ -360,22 +424,29 @@ public class WindowManagerProxy implements ResourceBasedOverride { } /** - * * Returns the display associated with the context, or DEFAULT_DISPLAY if the context isn't * associated with a display. */ protected Display getDisplay(Context displayInfoContext) { - if (Utilities.ATLEAST_R) { - try { - return displayInfoContext.getDisplay(); - } catch (UnsupportedOperationException e) { - // Ignore - } + try { + return displayInfoContext.getDisplay(); + } catch (UnsupportedOperationException e) { + // Ignore } return displayInfoContext.getSystemService(DisplayManager.class).getDisplay( DEFAULT_DISPLAY); } + /** + * Returns a DisplayCutout which represents a rotated version of the original + */ + protected DisplayCutout rotateCutout(DisplayCutout original, int startWidth, int startHeight, + int fromRotation, int toRotation) { + Rect safeCutout = getSafeInsets(original); + rotateRect(safeCutout, deltaRotation(fromRotation, toRotation)); + return new DisplayCutout(Insets.of(safeCutout), null, null, null, null); + } + /** * Returns the current navigation mode from resource. */ @@ -395,4 +466,12 @@ public class WindowManagerProxy implements ResourceBasedOverride { return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : NavigationMode.THREE_BUTTONS; } + + /** + * @see DisplayCutout#getSafeInsets + */ + public static Rect getSafeInsets(DisplayCutout cutout) { + return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } } diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index bef84f7bf3..31f5d6541a 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -19,7 +19,6 @@ import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR; import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON; import static com.android.launcher3.Utilities.allowBGLaunch; -import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_PENDING_INTENT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; @@ -150,7 +149,20 @@ public interface ActivityContext { * @return {@code true} if user has selected the first split app and is in the process of * selecting the second */ - default boolean isSplitSelectionEnabled() { + default boolean isSplitSelectionActive() { + // Overridden + return false; + } + + /** + * Handle user tapping on unsupported target when in split selection mode. + * See {@link #isSplitSelectionActive()} + * + * @return {@code true} if this method will handle the incorrect target selection, + * {@code false} if it could not be handled or if not possible to handle based on + * current split state + */ + default boolean handleIncorrectSplitTargetSelection() { // Overridden return false; } @@ -266,32 +278,26 @@ public interface ActivityContext { if (root == null) { return; } - 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) { - // Only hide the keyboard if it is actually showing. - if (isImeShown) { - StatsLogManager slm = getStatsLogManager(); - slm.keyboardStateManager().setKeyboardState(HIDE); - - // this method cannot be called cross threads - wic.hide(WindowInsets.Type.ime()); - slm.logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED); - } - - // If the WindowInsetsController is not null, we end here regardless of whether we - // hid the keyboard or not. - return; + 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) { + // Only hide the keyboard if it is actually showing. + if (isImeShown) { + // this method cannot be called cross threads + wic.hide(WindowInsets.Type.ime()); + getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED); } + + // If the WindowInsetsController is not null, we end here regardless of whether we hid + // the keyboard or not. + return; } InputMethodManager imm = root.getContext().getSystemService(InputMethodManager.class); @@ -316,8 +322,8 @@ public interface ActivityContext { } /** - * Returns if the software keyboard is hidden. Hardware keyboards do not display on screen by - * default. + * Returns if the software keyboard (including input toolbar) is hidden. Hardware + * keyboards do not display on screen by default. */ default boolean isSoftwareKeyboardHidden() { if (isHardwareKeyboard()) { diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java index a1cd697eee..5d2d3f4e9f 100644 --- a/src/com/android/launcher3/views/BaseDragLayer.java +++ b/src/com/android/launcher3/views/BaseDragLayer.java @@ -104,7 +104,7 @@ public abstract class BaseDragLayer protected final Rect mHitRect = new Rect(); @ViewDebug.ExportedProperty(category = "launcher") - private final RectF mSystemGestureRegion = new RectF(); + protected final RectF mSystemGestureRegion = new RectF(); private int mTouchDispatchState = 0; protected final T mActivity; @@ -164,7 +164,7 @@ public abstract class BaseDragLayer return findActiveController(ev); } - private boolean isEventInLauncher(MotionEvent ev) { + protected boolean isEventWithinSystemGestureRegion(MotionEvent ev) { final float x = ev.getX(); final float y = ev.getY(); @@ -175,7 +175,8 @@ public abstract class BaseDragLayer private TouchController findControllerToHandleTouch(MotionEvent ev) { AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); if (topView != null - && (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion()) + && (isEventWithinSystemGestureRegion(ev) + || topView.canInterceptEventsInSystemGestureRegion()) && topView.onControllerInterceptTouchEvent(ev)) { return topView; } @@ -287,7 +288,7 @@ public abstract class BaseDragLayer mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW | TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS; - if (isEventInLauncher(ev)) { + if (isEventWithinSystemGestureRegion(ev)) { mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION; } else { mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION; @@ -551,25 +552,21 @@ public abstract class BaseDragLayer @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { - if (Utilities.ATLEAST_Q) { - Insets gestureInsets = insets.getMandatorySystemGestureInsets(); - int gestureInsetBottom = gestureInsets.bottom; - Insets imeInset = Utilities.ATLEAST_R - ? insets.getInsets(WindowInsets.Type.ime()) - : Insets.NONE; - DeviceProfile dp = mActivity.getDeviceProfile(); - if (dp.isTaskbarPresent) { - // Ignore taskbar gesture insets to avoid interfering with TouchControllers. - gestureInsetBottom = ResourceUtils.getNavbarSize( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()); - } - mSystemGestureRegion.set( - Math.max(gestureInsets.left, imeInset.left), - Math.max(gestureInsets.top, imeInset.top), - Math.max(gestureInsets.right, imeInset.right), - Math.max(gestureInsetBottom, imeInset.bottom) - ); + Insets gestureInsets = insets.getMandatorySystemGestureInsets(); + int gestureInsetBottom = gestureInsets.bottom; + Insets imeInset = insets.getInsets(WindowInsets.Type.ime()); + DeviceProfile dp = mActivity.getDeviceProfile(); + if (dp.isTaskbarPresent) { + // Ignore taskbar gesture insets to avoid interfering with TouchControllers. + gestureInsetBottom = ResourceUtils.getNavbarSize( + ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()); } + mSystemGestureRegion.set( + Math.max(gestureInsets.left, imeInset.left), + Math.max(gestureInsets.top, imeInset.top), + Math.max(gestureInsets.right, imeInset.right), + Math.max(gestureInsetBottom, imeInset.bottom) + ); return super.dispatchApplyWindowInsets(insets); } } diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java index 87e496e4c2..5d3fa9bc4e 100644 --- a/src/com/android/launcher3/views/ClipIconView.java +++ b/src/com/android/launcher3/views/ClipIconView.java @@ -25,7 +25,6 @@ import static java.lang.Math.max; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -36,7 +35,6 @@ import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; @@ -55,7 +53,6 @@ import com.android.launcher3.graphics.IconShape; * Supports springing just the foreground layer. * Supports clipping the icon to/from its icon shape. */ -@TargetApi(Build.VERSION_CODES.Q) public class ClipIconView extends View implements ClipPathView { private static final Rect sTmpRect = new Rect(); @@ -112,7 +109,7 @@ public class ClipIconView extends View implements ClipPathView { float scaleY = rect.height() / minSize; float scale = Math.max(1f, Math.min(scaleX, scaleY)); - if (Float.isNaN(scale)) { + if (Float.isNaN(scale) || Float.isInfinite(scale)) { // Views are no longer laid out, do not update. return; } diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java index 32c70a3120..f76b53b90d 100644 --- a/src/com/android/launcher3/views/FloatingIconView.java +++ b/src/com/android/launcher3/views/FloatingIconView.java @@ -24,14 +24,12 @@ import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; import android.animation.Animator; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.CancellationSignal; import android.util.AttributeSet; import android.util.Log; @@ -67,7 +65,6 @@ import java.util.function.Supplier; /** * A view that is created to look like another view with the purpose of creating fluid animations. */ -@TargetApi(Build.VERSION_CODES.Q) public class FloatingIconView extends FrameLayout implements Animator.AnimatorListener, OnGlobalLayoutListener, FloatingView { diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java index bfb75f0022..c60e1a41cd 100644 --- a/src/com/android/launcher3/views/FloatingSurfaceView.java +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -18,14 +18,12 @@ package com.android.launcher3.views; import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView; import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Picture; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; -import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.SurfaceHolder; @@ -47,7 +45,6 @@ import com.android.launcher3.util.window.RefreshRateTracker; * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes * the surfaceHandle to the {@link GestureNavContract}. */ -@TargetApi(Build.VERSION_CODES.R) public class FloatingSurfaceView extends AbstractFloatingView implements OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 { @@ -178,7 +175,6 @@ public class FloatingSurfaceView extends AbstractFloatingView implements if (!mTmpPosition.equals(mIconPosition)) { mIconPosition.set(mTmpPosition); - sendIconInfo(); LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams(); lp.width = Math.round(mIconPosition.width()); @@ -187,6 +183,9 @@ public class FloatingSurfaceView extends AbstractFloatingView implements lp.topMargin = Math.round(mIconPosition.top); } } + + sendIconInfo(); + if (mIcon != null && iconChanged && !mIconBounds.isEmpty()) { // Record the icon display setCurrentIconVisible(true); @@ -200,7 +199,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements } private void sendIconInfo() { - if (mContract != null && !mIconPosition.isEmpty()) { + if (mContract != null) { mContract.sendEndPosition(mIconPosition, mLauncher, mSurfaceView.getSurfaceControl()); } } diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java index c0b24fa9f4..8408cc760d 100644 --- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -30,7 +30,6 @@ import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.util.Property; @@ -40,7 +39,6 @@ import android.view.ViewConfiguration; import android.view.WindowInsets; import android.widget.TextView; -import androidx.annotation.RequiresApi; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.FastScrollRecyclerView; @@ -352,26 +350,21 @@ public class RecyclerViewFastScroller extends View { float r = getScrollThumbRadius(); mThumbBounds.set(-halfW, 0, halfW, mThumbHeight); canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint); - if (Utilities.ATLEAST_Q) { - mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0)); - // swiping very close to the thumb area (not just within it's bound) - // will also prevent back gesture - SYSTEM_GESTURE_EXCLUSION_RECT.get(0).offset(mThumbDrawOffset.x, mThumbDrawOffset.y); - if (Utilities.ATLEAST_Q && mSystemGestureInsets != null) { - SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left = - SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right - mSystemGestureInsets.right; - } - setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); + mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0)); + // swiping very close to the thumb area (not just within it's bound) + // will also prevent back gesture + SYSTEM_GESTURE_EXCLUSION_RECT.get(0).offset(mThumbDrawOffset.x, mThumbDrawOffset.y); + if (mSystemGestureInsets != null) { + SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left = + SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right - mSystemGestureInsets.right; } + setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); canvas.restoreToCount(saveCount); } @Override - @RequiresApi(Build.VERSION_CODES.Q) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - if (Utilities.ATLEAST_Q) { - mSystemGestureInsets = insets.getSystemGestureInsets(); - } + mSystemGestureInsets = insets.getSystemGestureInsets(); return super.onApplyWindowInsets(insets); } diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java index 80b1cdd83c..4f5d31160a 100644 --- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java @@ -16,8 +16,6 @@ package com.android.launcher3.widget; -import static com.android.launcher3.Utilities.ATLEAST_R; - import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Insets; @@ -153,17 +151,10 @@ public class AddItemWidgetsBottomSheet extends AbstractSlideInView protected final Rect mInsets = new Rect(); - @Px protected int mContentHorizontalMargin; - @Px protected int mWidgetCellHorizontalPadding; + @Px + protected int mContentHorizontalMargin; + @Px + protected int mWidgetCellHorizontalPadding; protected int mNavBarScrimHeight; private final Paint mNavBarScrimPaint; @@ -72,14 +75,21 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mContentHorizontalMargin = getResources().getDimensionPixelSize( - R.dimen.widget_list_horizontal_margin); + mContentHorizontalMargin = getWidgetListHorizontalMargin(); mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize( R.dimen.widget_cell_horizontal_padding); mNavBarScrimPaint = new Paint(); mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext)); } + /** + * Returns the margins to be applied to the left and right of the widget apps list. + */ + protected int getWidgetListHorizontalMargin() { + return getResources().getDimensionPixelSize( + R.dimen.widget_list_horizontal_margin); + } + protected int getScrimColor(Context context) { return context.getResources().getColor(R.color.widgets_picker_scrim); } @@ -142,8 +152,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView @Override public void setInsets(Rect insets) { mInsets.set(insets); - @Px int contentHorizontalMargin = getResources().getDimensionPixelSize( - R.dimen.widget_list_horizontal_margin); + @Px int contentHorizontalMargin = getWidgetListHorizontalMargin(); if (contentHorizontalMargin != mContentHorizontalMargin) { onContentHorizontalMarginChanged(contentHorizontalMargin); mContentHorizontalMargin = contentHorizontalMargin; @@ -158,10 +167,8 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView private int getNavBarScrimHeight(WindowInsets insets) { if (mDisableNavBarScrim) { return 0; - } else if (Utilities.ATLEAST_Q) { - return insets.getTappableElementInsets().bottom; } else { - return insets.getStableInsetBottom(); + return insets.getTappableElementInsets().bottom; } } @@ -192,7 +199,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int widthUsed; if (deviceProfile.isTablet) { - widthUsed = Math.max(2 * getTabletMargin(deviceProfile), + widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile), 2 * (mInsets.left + mInsets.right)); } else if (mInsets.bottom > 0) { widthUsed = mInsets.left + mInsets.right; @@ -208,7 +215,11 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView MeasureSpec.getSize(heightMeasureSpec)); } - private int getTabletMargin(DeviceProfile deviceProfile) { + private int getTabletHorizontalMargin(DeviceProfile deviceProfile) { + // All bottom-sheets showing widgets will be full-width across all devices. + if (enableCategorizedWidgetSuggestions()) { + return 0; + } if (deviceProfile.isLandscape && !deviceProfile.isTwoPanels) { return getResources().getDimensionPixelSize( R.dimen.widget_picker_landscape_tablet_left_right_margin); diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java index 99485bec2e..aab78bd8c0 100644 --- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java +++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java @@ -41,9 +41,9 @@ import com.android.launcher3.Utilities; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.icons.ShadowGenerator; -import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.pm.ShortcutConfigActivityInfo; +import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.Executors; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.util.WidgetSizes; @@ -74,12 +74,12 @@ public class DatabaseWidgetPreviewLoader { * @return a request id which can be used to cancel the request. */ @NonNull - public HandlerRunnable loadPreview( + public CancellableTask loadPreview( @NonNull WidgetItem item, @NonNull Size previewSize, @NonNull Consumer callback) { Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler(); - HandlerRunnable request = new HandlerRunnable<>(handler, + CancellableTask request = new CancellableTask<>( () -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()), MAIN_EXECUTOR, callback); diff --git a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java deleted file mode 100644 index f42142ecf0..0000000000 --- a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.widget; - -import android.appwidget.AppWidgetProviderInfo; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.text.Layout; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.View; -import android.widget.RemoteViews; - -import com.android.launcher3.R; - -/** - * A widget host views created while the host has not bind to the system service. - */ -public class DeferredAppWidgetHostView extends LauncherAppWidgetHostView { - - private final TextPaint mPaint; - private Layout mSetupTextLayout; - - public DeferredAppWidgetHostView(Context context) { - super(context); - setWillNotDraw(false); - - mPaint = new TextPaint(); - mPaint.setColor(Color.WHITE); - mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, - mLauncher.getDeviceProfile().iconTextSizePx, - getResources().getDisplayMetrics())); - setBackgroundResource(R.drawable.bg_deferred_app_widget); - } - - @Override - public void updateAppWidget(RemoteViews remoteViews) { - // Not allowed - } - - @Override - public void addView(View child) { - // Not allowed - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - AppWidgetProviderInfo info = getAppWidgetInfo(); - if (info == null || TextUtils.isEmpty(info.label)) { - return; - } - - // 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; - } - 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((getWidth() - mSetupTextLayout.getWidth()) / 2, - (getHeight() - mSetupTextLayout.getHeight()) / 2); - mSetupTextLayout.draw(canvas); - } - } -} diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java index 9c21ea2bb7..40c39840d6 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java @@ -21,13 +21,22 @@ import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_I import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.util.Executors; +import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; import java.util.function.IntConsumer; /** @@ -37,8 +46,7 @@ import java.util.function.IntConsumer; */ class LauncherAppWidgetHost extends AppWidgetHost { @NonNull - private final ArrayList - mProviderChangeListeners = new ArrayList<>(); + private final List mProviderChangeListeners; @NonNull private final Context mContext; @@ -46,33 +54,16 @@ class LauncherAppWidgetHost extends AppWidgetHost { @Nullable private final IntConsumer mAppWidgetRemovedCallback; - @NonNull - private final LauncherWidgetHolder mHolder; + @Nullable + private ListenableHostView mViewToRecycle; public LauncherAppWidgetHost(@NonNull Context context, - @Nullable IntConsumer appWidgetRemovedCallback, @NonNull LauncherWidgetHolder holder) { + @Nullable IntConsumer appWidgetRemovedCallback, + List providerChangeListeners) { super(context, APPWIDGET_HOST_ID); mContext = context; mAppWidgetRemovedCallback = appWidgetRemovedCallback; - mHolder = holder; - } - - /** - * Add a listener that is triggered when the providers of the widgets are changed - * @param listener The listener that notifies when the providers changed - */ - public void addProviderChangeListener( - @NonNull LauncherWidgetHolder.ProviderChangedListener listener) { - mProviderChangeListeners.add(listener); - } - - /** - * Remove the specified listener from the host - * @param listener The listener that is to be removed from the host - */ - public void removeProviderChangeListener( - LauncherWidgetHolder.ProviderChangedListener listener) { - mProviderChangeListeners.remove(listener); + mProviderChangeListeners = providerChangeListeners; } @Override @@ -85,11 +76,21 @@ class LauncherAppWidgetHost extends AppWidgetHost { } } + /** + * Sets the view to be recycled for the next widget creation. + */ + public void recycleViewForNextCreation(ListenableHostView viewToRecycle) { + mViewToRecycle = viewToRecycle; + } + @Override @NonNull public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { - return mHolder.onCreateView(context, appWidgetId, appWidget); + ListenableHostView result = + mViewToRecycle != null ? mViewToRecycle : new ListenableHostView(context); + mViewToRecycle = null; + return result; } /** @@ -115,7 +116,10 @@ class LauncherAppWidgetHost extends AppWidgetHost { if (mAppWidgetRemovedCallback == null) { return; } - mAppWidgetRemovedCallback.accept(appWidgetId); + // Route the call via model thread, in case it comes while a loader-bind is in progress + Executors.MODEL_EXECUTOR.execute( + () -> Executors.MAIN_EXECUTOR.execute( + () -> mAppWidgetRemovedCallback.accept(appWidgetId))); } /** @@ -126,4 +130,36 @@ class LauncherAppWidgetHost extends AppWidgetHost { super.clearViews(); } + public static class ListenableHostView extends LauncherAppWidgetHostView { + + private Set mUpdateListeners = Collections.EMPTY_SET; + + ListenableHostView(Context context) { + super(context); + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + super.updateAppWidget(remoteViews); + mUpdateListeners.forEach(Runnable::run); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(LauncherAppWidgetHostView.class.getName()); + } + + /** + * Adds a callback to be run everytime the provided app widget updates. + * @return a closable to remove this callback + */ + public SafeCloseable addUpdateListener(Runnable callback) { + if (mUpdateListeners == Collections.EMPTY_SET) { + mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>()); + } + mUpdateListeners.add(callback); + return () -> mUpdateListeners.remove(callback); + } + } } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index 5d069ed264..2259e3c0cd 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -16,11 +16,9 @@ package com.android.launcher3.widget; -import android.annotation.TargetApi; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.graphics.Rect; -import android.os.Build; import android.os.Handler; import android.os.Parcelable; import android.os.SystemClock; @@ -41,14 +39,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.CheckLongPressHelper; -import com.android.launcher3.Launcher; +import com.android.launcher3.Flags; import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; /** @@ -75,7 +72,7 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView private final Rect mTempRect = new Rect(); private final CheckLongPressHelper mLongPressHelper; - protected final Launcher mLauncher; + protected final ActivityContext mActivityContext; // Maintain the color manager. private final LocalColorExtractor mColorExtractor; @@ -95,16 +92,17 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView private boolean mTrackingWidgetUpdate = false; - private boolean mIsWidgetCachingDisabled = false; - public LauncherAppWidgetHostView(Context context) { super(context); - mLauncher = Launcher.getLauncher(context); + mActivityContext = ActivityContext.lookupContext(context); mLongPressHelper = new CheckLongPressHelper(this, this); - setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); + setAccessibilityDelegate(mActivityContext.getAccessibilityDelegate()); setBackgroundResource(R.drawable.widget_internal_focus_bg); + if (Flags.enableFocusOutline()) { + setDefaultFocusHighlightEnabled(false); + } - if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { + if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) { setOnLightBackground(true); } mColorExtractor = new LocalColorExtractor(); // no-op @@ -122,50 +120,35 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView @Override public boolean onLongClick(View view) { if (mIsScrollable) { - DragLayer dragLayer = mLauncher.getDragLayer(); - dragLayer.requestDisallowInterceptTouchEvent(false); + mActivityContext.getDragLayer().requestDisallowInterceptTouchEvent(false); } view.performLongClick(); return true; } @Override - @TargetApi(Build.VERSION_CODES.Q) public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { super.setAppWidget(appWidgetId, info); - if (!mTrackingWidgetUpdate && Utilities.ATLEAST_Q) { + if (!mTrackingWidgetUpdate) { mTrackingWidgetUpdate = true; Trace.beginAsyncSection(TRACE_METHOD_NAME + info.provider, appWidgetId); Log.i(TAG, "App widget created with id: " + appWidgetId); } } - public void setIsWidgetCachingDisabled(boolean isWidgetCachingDisabled) { - mIsWidgetCachingDisabled = isWidgetCachingDisabled; - } - @Override - @TargetApi(Build.VERSION_CODES.Q) public void updateAppWidget(RemoteViews remoteViews) { - if (mTrackingWidgetUpdate && remoteViews != null && Utilities.ATLEAST_Q) { + if (mTrackingWidgetUpdate && remoteViews != null) { Log.i(TAG, "App widget with id: " + getAppWidgetId() + " loaded"); Trace.endAsyncSection( TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId()); mTrackingWidgetUpdate = false; } - if (FeatureFlags.ENABLE_CACHED_WIDGET.get() - && !mIsWidgetCachingDisabled) { + if (isDeferringUpdates()) { mLastRemoteViews = remoteViews; - if (isDeferringUpdates()) { - return; - } - } else { - if (isDeferringUpdates()) { - mLastRemoteViews = remoteViews; - return; - } - mLastRemoteViews = null; + return; } + mLastRemoteViews = null; super.updateAppWidget(remoteViews); @@ -234,7 +217,7 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { - DragLayer dragLayer = mLauncher.getDragLayer(); + BaseDragLayer dragLayer = mActivityContext.getDragLayer(); if (mIsScrollable) { dragLayer.requestDisallowInterceptTouchEvent(true); } @@ -299,8 +282,7 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView super.onLayout(changed, left, top, right, bottom); mIsScrollable = checkScrollableRecursively(this); - if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) { - LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); + if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo info) { mTempRect.set(left, top, right, bottom); mColorExtractor.setWorkspaceLocation(mTempRect, (View) getParent(), info.screenId); } @@ -434,26 +416,9 @@ public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView scheduleNextAdvance(); } - public void reInflate() { - if (!isAttachedToWindow()) { - return; - } - LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); - if (info == null) { - // This occurs when LauncherAppWidgetHostView is used to render a preview layout. - return; - } - // 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 */, - "widget removed because of configuration change"); - mLauncher.bindAppWidget(info); - } - @Override protected boolean shouldAllowDirectClick() { - if (getTag() instanceof ItemInfo) { - ItemInfo item = (ItemInfo) getTag(); + if (getTag() instanceof ItemInfo item) { return item.spanX == 1 && item.spanY == 1; } return false; diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java index ef51d152c7..3e4fd8caa8 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java @@ -15,7 +15,6 @@ import android.os.UserHandle; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.Utilities; import com.android.launcher3.icons.ComponentWithLabelAndIcon; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -206,11 +205,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo } public int getWidgetFeatures() { - if (Utilities.ATLEAST_P) { - return widgetFeatures; - } else { - return 0; - } + return widgetFeatures; } public boolean isReconfigurable() { diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java index 6acc83d1d1..15bd6ed1f2 100644 --- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java +++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java @@ -17,7 +17,9 @@ package com.android.launcher3.widget; import static android.app.Activity.RESULT_CANCELED; +import static com.android.launcher3.Flags.enableWorkspaceInflation; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; @@ -27,8 +29,8 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.Looper; import android.util.SparseArray; -import android.widget.RemoteViews; import android.widget.Toast; import androidx.annotation.NonNull; @@ -36,17 +38,19 @@ import androidx.annotation.Nullable; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView; import com.android.launcher3.widget.custom.CustomWidgetManager; +import java.util.ArrayList; +import java.util.List; import java.util.function.IntConsumer; /** @@ -64,17 +68,14 @@ public class LauncherWidgetHolder { FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED; @NonNull - private final Context mContext; + protected final Context mContext; @NonNull private final AppWidgetHost mWidgetHost; @NonNull - private final SparseArray mViews = new SparseArray<>(); - @NonNull - private final SparseArray mPendingViews = new SparseArray<>(); - @NonNull - private final SparseArray mDeferredViews = new SparseArray<>(); + protected final SparseArray mViews = new SparseArray<>(); + protected final List mProviderChangedListeners = new ArrayList<>(); protected int mFlags = FLAG_STATE_IS_NORMAL; @@ -91,7 +92,8 @@ public class LauncherWidgetHolder { protected AppWidgetHost createHost( Context context, @Nullable IntConsumer appWidgetRemovedCallback) { - return new LauncherAppWidgetHost(context, appWidgetRemovedCallback, this); + return new LauncherAppWidgetHost( + context, appWidgetRemovedCallback, mProviderChangedListeners); } /** @@ -121,25 +123,12 @@ public class LauncherWidgetHolder { * Update any views which have been deferred because the host was not listening. */ protected void updateDeferredView() { + // Update any views which have been deferred because the host was not listening. // We go in reverse order and inflate any deferred or cached widget for (int i = mViews.size() - 1; i >= 0; i--) { LauncherAppWidgetHostView view = mViews.valueAt(i); - if (view instanceof DeferredAppWidgetHostView) { - view.reInflate(); - } - if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { - final int appWidgetId = mViews.keyAt(i); - if (view == mDeferredViews.get(appWidgetId)) { - // If the widget view was deferred, we'll need to call super.createView here - // to make the binder call to system process to fetch cumulative updates to this - // widget, as well as setting up this view for future updates. - mWidgetHost.createView(view.mLauncher, appWidgetId, - view.getAppWidgetInfo()); - // At this point #onCreateView should have been called, which in turn returned - // the deferred view. There's no reason to keep the reference anymore, so we - // removed it here. - mDeferredViews.remove(appWidgetId); - } + if (view instanceof PendingAppWidgetHostView pv) { + pv.reInflate(); } } } @@ -173,34 +162,6 @@ public class LauncherWidgetHolder { public void deleteAppWidgetId(int appWidgetId) { mWidgetHost.deleteAppWidgetId(appWidgetId); mViews.remove(appWidgetId); - if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { - final LauncherAppState state = LauncherAppState.getInstance(mContext); - synchronized (state.mCachedRemoteViews) { - state.mCachedRemoteViews.delete(appWidgetId); - } - } - } - - /** - * Add the pending view to the host for complete configuration in further steps - * @param appWidgetId The ID of the specified app widget - * @param view The {@link PendingAppWidgetHostView} of the app widget - */ - public void addPendingView(int appWidgetId, @NonNull PendingAppWidgetHostView view) { - mPendingViews.put(appWidgetId, view); - } - - /** - * @param appWidgetId The app widget id of the specified widget - * @return The {@link PendingAppWidgetHostView} of the widget if it exists, null otherwise - */ - @Nullable - protected PendingAppWidgetHostView getPendingView(int appWidgetId) { - return mPendingViews.get(appWidgetId); - } - - protected void removePendingView(int appWidgetId) { - mPendingViews.remove(appWidgetId); } /** @@ -225,18 +186,18 @@ public class LauncherWidgetHolder { * Add a listener that is triggered when the providers of the widgets are changed * @param listener The listener that notifies when the providers changed */ - public void addProviderChangeListener(@NonNull ProviderChangedListener listener) { - LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost; - tempHost.addProviderChangeListener(listener); + public void addProviderChangeListener( + @NonNull LauncherWidgetHolder.ProviderChangedListener listener) { + MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.add(listener)); } /** * Remove the specified listener from the host * @param listener The listener that is to be removed from the host */ - public void removeProviderChangeListener(ProviderChangedListener listener) { - LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost; - tempHost.removeProviderChangeListener(listener); + public void removeProviderChangeListener( + LauncherWidgetHolder.ProviderChangedListener listener) { + MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.remove(listener)); } /** @@ -319,17 +280,6 @@ public class LauncherWidgetHolder { if (WidgetsModel.GO_DISABLE_WIDGETS) { return; } - if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { - // Cache the content from the widgets when Launcher stops listening to widget updates - final LauncherAppState state = LauncherAppState.getInstance(mContext); - synchronized (state.mCachedRemoteViews) { - for (int i = 0; i < mViews.size(); i++) { - final int appWidgetId = mViews.keyAt(i); - final LauncherAppWidgetHostView view = mViews.get(appWidgetId); - state.mCachedRemoteViews.put(appWidgetId, view.mLastRemoteViews); - } - } - } mWidgetHost.stopListening(); setListeningFlag(false); } @@ -351,47 +301,108 @@ public class LauncherWidgetHolder { } /** - * Create a view for the specified app widget - * @param context The activity context for which the view is created + * Adds a callback to be run everytime the provided app widget updates. + * @return a closable to remove this callback + */ + public SafeCloseable addOnUpdateListener( + int appWidgetId, LauncherAppWidgetProviderInfo appWidget, Runnable callback) { + if (createView(appWidgetId, appWidget) instanceof ListenableHostView lhv) { + return lhv.addUpdateListener(callback); + } + return () -> { }; + } + + /** + * Create a view for the specified app widget. When calling this method from a background + * thread, the returned view will not receive ongoing updates. The caller needs to reattach + * the view using {@link #attachViewToHostAndGetAttachedView} on UIThread + * * @param appWidgetId The ID of the widget - * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget + * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget * @return A view for the widget */ @NonNull - public AppWidgetHostView createView(@NonNull Context context, int appWidgetId, - @NonNull LauncherAppWidgetProviderInfo appWidget) { + public AppWidgetHostView createView( + int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { if (appWidget.isCustomWidget()) { - LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context); + LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(mContext); lahv.setAppWidget(0, appWidget); - CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv); + CustomWidgetManager.INSTANCE.get(mContext).onViewCreated(lahv); return lahv; - } else if ((mFlags & FLAG_LISTENING) == 0) { - // Since the launcher hasn't started listening to widget updates, we can't simply call - // super.createView here because the later will make a binder call to retrieve - // RemoteViews from system process. - // TODO: have launcher always listens to widget updates in background so that this - // check can be removed altogether. - if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { - final RemoteViews cachedRemoteViews = getCachedRemoteViews(appWidgetId); - if (cachedRemoteViews != null) { - // We've found RemoteViews from cache for this widget, so we will instantiate a - // widget host view and populate it with the cached RemoteViews. - final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context); - view.setAppWidget(appWidgetId, appWidget); - view.updateAppWidget(cachedRemoteViews); - mDeferredViews.put(appWidgetId, view); - mViews.put(appWidgetId, view); - return view; - } - } - // If cache misses or not enabled, a placeholder for the widget will be returned. - DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context); - view.setAppWidget(appWidgetId, appWidget); + } + + LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget); + // Do not update mViews on a background thread call, as the holder is not thread safe. + if (!enableWorkspaceInflation() || Looper.myLooper() == Looper.getMainLooper()) { mViews.put(appWidgetId, view); - return view; + } + return view; + } + + /** + * Attaches an already inflated view to the host. If the view can't be attached, creates + * and attaches a new view. + * @return the final attached view + */ + @NonNull + public final AppWidgetHostView attachViewToHostAndGetAttachedView( + @NonNull LauncherAppWidgetHostView view) { + if (mViews.get(view.getAppWidgetId()) != view) { + view = recycleExistingView(view); + mViews.put(view.getAppWidgetId(), view); + } + return view; + } + + /** + * Recycling logic: + * 1) If the final view should be a pendingView + * if the provided view is also a pendingView, return itself + * otherwise discard provided view and return a new pending view + * 2) If the recycled view is a pendingView, discard it and return a new view + * 3) Use the same for as creating a new view, but used the provided view in the host instead + * of creating a new view. This ensures that all the host callbacks are properly attached + * as a result of using the same flow. + */ + protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) { + if ((mFlags & FLAG_LISTENING) == 0) { + if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) { + return view; + } else { + return new PendingAppWidgetHostView(mContext, this, view.getAppWidgetId(), + fromProviderInfo(mContext, view.getAppWidgetInfo())); + } + } + LauncherAppWidgetHost host = (LauncherAppWidgetHost) mWidgetHost; + if (view instanceof ListenableHostView lhv) { + host.recycleViewForNextCreation(lhv); + } + + view = createViewInternal( + view.getAppWidgetId(), fromProviderInfo(mContext, view.getAppWidgetInfo())); + host.recycleViewForNextCreation(null); + return view; + } + + @NonNull + protected LauncherAppWidgetHostView createViewInternal( + int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { + if ((mFlags & FLAG_LISTENING) == 0) { + // Since the launcher hasn't started listening to widget updates, we can't simply call + // host.createView here because the later will make a binder call to retrieve + // RemoteViews from system process. + return new PendingAppWidgetHostView(mContext, this, appWidgetId, appWidget); } else { + if (enableWorkspaceInflation() && Looper.myLooper() != Looper.getMainLooper()) { + // Widget is being inflated a background thread, just create and + // return a placeholder view + ListenableHostView hostView = new ListenableHostView(mContext); + hostView.setAppWidget(appWidgetId, appWidget); + return hostView; + } try { - return mWidgetHost.createView(context, appWidgetId, appWidget); + return (LauncherAppWidgetHostView) mWidgetHost.createView( + mContext, appWidgetId, appWidget); } catch (Exception e) { if (!Utilities.isBinderSizeError(e)) { throw new RuntimeException(e); @@ -402,7 +413,7 @@ public class LauncherWidgetHolder { // will update. LauncherAppWidgetHostView view = mViews.get(appWidgetId); if (view == null) { - view = onCreateView(mContext, appWidgetId, appWidget); + view = new ListenableHostView(mContext); } view.setAppWidget(appWidgetId, appWidget); view.switchToErrorView(); @@ -421,42 +432,12 @@ public class LauncherWidgetHolder { void notifyWidgetProvidersChanged(); } - /** - * Called to return a proper view when creating a view - * @param context The context for which the widget view is created - * @param appWidgetId The ID of the added widget - * @param appWidget The provider info of the added widget - * @return A view for the specified app widget - */ - @NonNull - public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId, - AppWidgetProviderInfo appWidget) { - final LauncherAppWidgetHostView view; - if (getPendingView(appWidgetId) != null) { - view = getPendingView(appWidgetId); - removePendingView(appWidgetId); - } else if (mDeferredViews.get(appWidgetId) != null) { - // In case the widget view is deferred, we will simply return the deferred view as - // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher - // already added the former to the workspace. - view = mDeferredViews.get(appWidgetId); - } else { - view = new LauncherAppWidgetHostView(context); - } - mViews.put(appWidgetId, view); - return view; - } - /** * Clears all the views from the host */ public void clearViews() { LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost; tempHost.clearViews(); - if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { - // Clear previously cached content from existing widgets - mDeferredViews.clear(); - } mViews.clear(); } @@ -496,14 +477,6 @@ public class LauncherWidgetHolder { return (flags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN; } - @Nullable - private RemoteViews getCachedRemoteViews(int appWidgetId) { - final LauncherAppState state = LauncherAppState.getInstance(mContext); - synchronized (state.mCachedRemoteViews) { - return state.mCachedRemoteViews.get(appWidgetId); - } - } - /** * Returns the new LauncherWidgetHolder instance */ diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index ccf4b2ea04..a501960920 100644 --- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -27,6 +27,7 @@ import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.widget.picker.WidgetRecommendationCategory; import com.android.launcher3.widget.util.WidgetSizes; /** @@ -42,6 +43,16 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo { public Bundle bindOptions = null; public int sourceContainer; + public WidgetRecommendationCategory recommendationCategory = null; + + public PendingAddWidgetInfo( + LauncherAppWidgetProviderInfo i, + int container, + WidgetRecommendationCategory recommendationCategory) { + this(i, container); + this.recommendationCategory = recommendationCategory; + } + public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, int container) { if (i.isCustomWidget()) { itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java index 1c88c4a466..86400baaf2 100644 --- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java @@ -16,19 +16,29 @@ package com.android.launcher3.widget; +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; +import android.text.TextUtils; import android.util.SizeF; import android.util.TypedValue; import android.view.ContextThemeWrapper; @@ -36,17 +46,19 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.RemoteViews; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.icons.FastBitmapDrawable; -import com.android.launcher3.icons.IconCache; import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.PackageItemInfo; -import com.android.launcher3.touch.ItemClickHandler; +import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.Themes; import java.util.List; @@ -54,55 +66,175 @@ import java.util.List; public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implements OnClickListener, ItemInfoUpdateReceiver { private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5; - private static final float MIN_SATUNATION = 0.7f; + private static final float MIN_SATURATION = 0.7f; + + private static final int FLAG_DRAW_SETTINGS = 1; + private static final int FLAG_DRAW_ICON = 2; + private static final int FLAG_DRAW_LABEL = 4; + + private static final int DEFERRED_ALPHA = 0x77; private final Rect mRect = new Rect(); - private OnClickListener mClickListener; + + private final LauncherWidgetHolder mWidgetHolder; + private final LauncherAppWidgetProviderInfo mAppwidget; private final LauncherAppWidgetInfo mInfo; private final int mStartState; private final boolean mDisabledForSafeMode; + private final CharSequence mLabel; + + private OnClickListener mClickListener; + private SafeCloseable mOnDetachCleanup; + + private int mDragFlags; private Drawable mCenterDrawable; private Drawable mSettingIconDrawable; private boolean mDrawableSizeChanged; + private boolean mIsDeferredWidget; private final TextPaint mPaint; + + private final Paint mPreviewPaint; private Layout mSetupTextLayout; - public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info, - IconCache cache, boolean disabledForSafeMode) { - super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme)); + @Nullable private Bitmap mPreviewBitmap; - mInfo = info; - mStartState = info.restoreStatus; - mDisabledForSafeMode = disabledForSafeMode; - - mPaint = new TextPaint(); - mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary)); - mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, - mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); - setBackgroundResource(R.drawable.pending_widget_bg); - setWillNotDraw(false); + public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, + LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) { + this(context, widgetHolder, info, appWidget, + context.getResources().getText(R.string.gadget_complete_setup_text)); super.updateAppWidget(null); - setOnClickListener(mLauncher.getItemOnClickListener()); + setOnClickListener(mActivityContext.getItemOnClickListener()); if (info.pendingItemInfo == null) { info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(), info.user); - cache.updateIconInBackground(this, info.pendingItemInfo); + LauncherAppState.getInstance(context).getIconCache() + .updateIconInBackground(this, info.pendingItemInfo); } else { reapplyItemInfo(info.pendingItemInfo); } } + public PendingAppWidgetHostView( + Context context, LauncherWidgetHolder widgetHolder, + int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { + this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider), + appWidget, appWidget.label); + getBackground().mutate().setAlpha(DEFERRED_ALPHA); + + mCenterDrawable = new ColorDrawable(Color.TRANSPARENT); + mDragFlags = FLAG_DRAW_LABEL; + mDrawableSizeChanged = true; + mIsDeferredWidget = true; + } + + /** Set {@link Bitmap} of widget preview. */ + public void setPreviewBitmap(@Nullable Bitmap previewBitmap) { + if (this.mPreviewBitmap == previewBitmap) { + return; + } + this.mPreviewBitmap = previewBitmap; + invalidate(); + } + + private PendingAppWidgetHostView(Context context, + LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, + LauncherAppWidgetProviderInfo appwidget, CharSequence label) { + super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme)); + mWidgetHolder = widgetHolder; + mAppwidget = appwidget; + mInfo = info; + mStartState = info.restoreStatus; + mDisabledForSafeMode = LauncherAppState.getInstance(context).isSafeModeEnabled(); + mLabel = label; + + mPaint = new TextPaint(); + mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary)); + mPaint.setTextSize(TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_PX, + mActivityContext.getDeviceProfile().iconTextSizePx, + getResources().getDisplayMetrics())); + mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG); + + setWillNotDraw(false); + setBackgroundResource(R.drawable.pending_widget_bg); + } + + @Override + public AppWidgetProviderInfo getAppWidgetInfo() { + return mAppwidget; + } + + @Override + public int getAppWidgetId() { + return mInfo.appWidgetId; + } + @Override public void updateAppWidget(RemoteViews remoteViews) { + checkIfRestored(); + } + + private void checkIfRestored() { WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext()); if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) { - super.updateAppWidget(remoteViews); - reInflate(); + MAIN_EXECUTOR.getHandler().post(this::reInflate); + } + } + + public boolean isDeferredWidget() { + return mIsDeferredWidget; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if ((mAppwidget != null) + && !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) + && mInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) { + // If the widget is not completely restored, but has a valid ID, then listen of + // updates from provider app for potential restore complete. + if (mOnDetachCleanup != null) { + mOnDetachCleanup.close(); + } + mOnDetachCleanup = mWidgetHolder.addOnUpdateListener( + mInfo.appWidgetId, mAppwidget, this::checkIfRestored); + checkIfRestored(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mOnDetachCleanup != null) { + mOnDetachCleanup.close(); + mOnDetachCleanup = null; + } + } + + /** + * Forces the Launcher to reinflate the widget view + */ + public void reInflate() { + if (!isAttachedToWindow()) { + return; + } + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); + if (info == null) { + // This occurs when LauncherAppWidgetHostView is used to render a preview layout. + return; + } + if (mActivityContext instanceof Launcher launcher) { + // Remove and rebind the current widget (which was inflated in the wrong + // orientation), but don't delete it from the database + launcher.removeItem(this, info, false /* deleteFromDb */, + "widget removed because of configuration change"); + launcher.bindAppWidget(info); } } @@ -147,7 +279,10 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView mCenterDrawable.setCallback(null); mCenterDrawable = null; } + mDragFlags = 0; if (info.bitmap.icon != null) { + mDragFlags = FLAG_DRAW_ICON; + Drawable widgetCategoryIcon = getWidgetCategoryIcon(); // The view displays three modes, // 1) App icon in the center @@ -169,6 +304,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView : widgetCategoryIcon; mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate(); updateSettingColor(info.bitmap.color); + + mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL; } else { mCenterDrawable = widgetCategoryIcon == null ? newPendingIcon(getContext(), info) @@ -186,7 +323,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView // Make the dominant color bright. float[] hsv = new float[3]; Color.colorToHSV(dominantColor, hsv); - hsv[1] = Math.min(hsv[1], MIN_SATUNATION); + hsv[1] = Math.min(hsv[1], MIN_SATURATION); hsv[2] = 1; mSettingIconDrawable.setColorFilter(Color.HSVToColor(hsv), PorterDuff.Mode.SRC_IN); } @@ -227,7 +364,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView } private void updateDrawableBounds() { - DeviceProfile grid = mLauncher.getDeviceProfile(); + DeviceProfile grid = mActivityContext.getDeviceProfile(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); int paddingLeft = getPaddingLeft(); @@ -239,73 +376,73 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView int availableWidth = getWidth() - paddingLeft - paddingRight - 2 * minPadding; int availableHeight = getHeight() - paddingTop - paddingBottom - 2 * minPadding; - if (mSettingIconDrawable == null) { - int maxSize = grid.iconSizePx; - int size = Math.min(maxSize, Math.min(availableWidth, availableHeight)); + float iconSize = ((mDragFlags & FLAG_DRAW_ICON) == 0) ? 0 + : Math.max(0, Math.min(availableWidth, availableHeight)); + // Use twice the setting size factor, as the setting is drawn at a corner and the + // icon is drawn in the center. + float settingIconScaleFactor = ((mDragFlags & FLAG_DRAW_SETTINGS) == 0) ? 0 + : 1 + SETUP_ICON_SIZE_FACTOR * 2; - mRect.set(0, 0, size, size); - mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); - mCenterDrawable.setBounds(mRect); - } else { - float iconSize = Math.max(0, Math.min(availableWidth, availableHeight)); + int maxSize = Math.max(availableWidth, availableHeight); + if (iconSize * settingIconScaleFactor > maxSize) { + // There is an overlap + iconSize = maxSize / settingIconScaleFactor; + } - // Use twice the setting size factor, as the setting is drawn at a corner and the - // icon is drawn in the center. - float settingIconScaleFactor = 1 + SETUP_ICON_SIZE_FACTOR * 2; - int maxSize = Math.max(availableWidth, availableHeight); - if (iconSize * settingIconScaleFactor > maxSize) { - // There is an overlap - iconSize = maxSize / settingIconScaleFactor; + int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx); + + // Icon top when we do not draw the text + int iconTop = (getHeight() - actualIconSize) / 2; + mSetupTextLayout = null; + + if (availableWidth > 0 && !TextUtils.isEmpty(mLabel) + && ((mDragFlags & FLAG_DRAW_LABEL) != 0)) { + // Recreate the setup text. + mSetupTextLayout = new StaticLayout( + mLabel, mPaint, availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true); + int textHeight = mSetupTextLayout.getHeight(); + + // Extra icon size due to the setting icon + float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor + + grid.iconDrawablePaddingPx; + + if (minHeightWithText < availableHeight) { + // We can draw the text as well + iconTop = (getHeight() - textHeight + - grid.iconDrawablePaddingPx - actualIconSize) / 2; + + } else { + // We can't draw the text. Let the iconTop be same as before. + mSetupTextLayout = null; } + } - int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx); - - // Icon top when we do not draw the text - int iconTop = (getHeight() - actualIconSize) / 2; - mSetupTextLayout = null; - - if (availableWidth > 0) { - // Recreate the setup text. - mSetupTextLayout = new StaticLayout( - getResources().getText(R.string.gadget_complete_setup_text), mPaint, - availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true); - int textHeight = mSetupTextLayout.getHeight(); - - // Extra icon size due to the setting icon - float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor - + grid.iconDrawablePaddingPx; - - if (minHeightWithText < availableHeight) { - // We can draw the text as well - iconTop = (getHeight() - textHeight - - grid.iconDrawablePaddingPx - actualIconSize) / 2; - - } else { - // We can't draw the text. Let the iconTop be same as before. - mSetupTextLayout = null; - } - } - - mRect.set(0, 0, actualIconSize, actualIconSize); - mRect.offset((getWidth() - actualIconSize) / 2, iconTop); - mCenterDrawable.setBounds(mRect); + mRect.set(0, 0, actualIconSize, actualIconSize); + mRect.offset((getWidth() - actualIconSize) / 2, iconTop); + mCenterDrawable.setBounds(mRect); + if (mSettingIconDrawable != null) { mRect.left = paddingLeft + minPadding; mRect.right = mRect.left + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); mRect.top = paddingTop + minPadding; mRect.bottom = mRect.top + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); mSettingIconDrawable.setBounds(mRect); + } - if (mSetupTextLayout != null) { - // Set up position for dragging the text - mRect.left = paddingLeft + minPadding; - mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx; - } + if (mSetupTextLayout != null) { + // Set up position for dragging the text + mRect.left = paddingLeft + minPadding; + mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx; } } @Override protected void onDraw(Canvas canvas) { + if (mPreviewBitmap != null + && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) { + canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint); + return; + } if (mCenterDrawable == null) { // Nothing to draw return; diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index c30342ad4b..f2f83c8e5d 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -16,6 +16,8 @@ package com.android.launcher3.widget; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; + import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY; import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx; @@ -44,12 +46,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.RoundDrawableWrapper; -import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.util.CancellableTask; import com.android.launcher3.views.ActivityContext; import java.util.function.Consumer; @@ -87,7 +93,7 @@ public class WidgetCell extends LinearLayout { private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader; - protected HandlerRunnable mActiveRequest; + protected CancellableTask mActiveRequest; private boolean mAnimatePreview = true; protected final ActivityContext mActivity; @@ -99,6 +105,8 @@ public class WidgetCell extends LinearLayout { private float mAppWidgetHostViewScale = 1f; private int mSourceContainer = CONTAINER_WIDGETS_TRAY; + private CancellableTask mIconLoadRequest; + public WidgetCell(Context context) { this(context, null); } @@ -147,6 +155,11 @@ public class WidgetCell extends LinearLayout { return mAppWidgetHostViewScale; } + /** Returns the {@link WidgetItem} for this {@link WidgetCell}. */ + public WidgetItem getWidgetItem() { + return mItem; + } + /** * Called to clear the view and free attached resources. (e.g., {@link Bitmap} */ @@ -177,6 +190,7 @@ public class WidgetCell extends LinearLayout { mPreviewContainerScale = 1f; mItem = null; mWidgetSize = new Size(0, 0); + showAppIconInWidgetTitle(false); } public void setSourceContainer(int sourceContainer) { @@ -236,6 +250,11 @@ public class WidgetCell extends LinearLayout { mAppWidgetHostViewPreview = createAppWidgetHostView(context); setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, mRemoteViewsPreview); + } else if (Flags.enableGeneratedPreviews() + && item.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)) { + mAppWidgetHostViewPreview = createAppWidgetHostView(context); + setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, + item.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN)); } else if (item.hasPreviewLayout()) { // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, @@ -354,6 +373,41 @@ public class WidgetCell extends LinearLayout { } } + /** + * Shows or hides the long description displayed below each widget. + * + * @param show a flag that shows the long description of the widget if {@code true}, hides it if + * {@code false}. + */ + public void showDescription(boolean show) { + mWidgetDescription.setVisibility(show ? VISIBLE : GONE); + } + + /** + * Set whether the app icon, for the app that provides the widget, should be shown next to the + * title text of the widget. + * + * @param show true if the app icon should be shown in the title text of the cell, false hides + * it. + */ + public void showAppIconInWidgetTitle(boolean show) { + if (show) { + if (mItem.widgetInfo != null) { + loadHighResPackageIcon(); + + Drawable icon = mItem.bitmap.newIcon(getContext()); + int size = getResources().getDimensionPixelSize(R.dimen.widget_cell_app_icon_size); + icon.setBounds(0, 0, size, size); + mWidgetName.setCompoundDrawablesRelative( + icon, + null, null, null); + } + } else { + cancelIconLoadRequest(); + mWidgetName.setCompoundDrawables(null, null, null, null); + } + } + @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); @@ -407,4 +461,38 @@ public class WidgetCell extends LinearLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + + /** + * Loads a high resolution package icon to show next to the widget title. + */ + public void loadHighResPackageIcon() { + cancelIconLoadRequest(); + if (mItem.bitmap.isLowRes()) { + // We use the package icon instead of the receiver one so that the overall package that + // the widget came from can be identified in the recommended widgets. This matches with + // the package icon headings in the all widgets list. + PackageItemInfo tmpPackageItem = new PackageItemInfo( + mItem.componentName.getPackageName(), + mItem.user); + mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() + .updateIconInBackground(this::reapplyIconInfo, tmpPackageItem); + } + } + + /** Can be called to update the package icon shown in the label of recommended widgets. */ + private void reapplyIconInfo(ItemInfoWithIcon info) { + if (mItem == null || info.bitmap.isNullOrLowRes()) { + showAppIconInWidgetTitle(false); + return; + } + mItem.bitmap = info.bitmap; + showAppIconInWidgetTitle(true); + } + + private void cancelIconLoadRequest() { + if (mIconLoadRequest != null) { + mIconLoadRequest.cancel(); + mIconLoadRequest = null; + } + } } diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java index b18cd471cb..1cc00ef6cb 100644 --- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -1,7 +1,6 @@ package com.android.launcher3.widget; import android.appwidget.AppWidgetHostView; -import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -117,7 +116,7 @@ public class WidgetHostViewLoader implements DragController.DragListener { return; } AppWidgetHostView hostView = mLauncher.getAppWidgetHolder().createView( - (Context) mLauncher, mWidgetLoadingId, pInfo); + mWidgetLoadingId, pInfo); mInfo.boundWidget = hostView; // We used up the widget Id in binding the above view. diff --git a/src/com/android/launcher3/widget/WidgetInflater.kt b/src/com/android/launcher3/widget/WidgetInflater.kt new file mode 100644 index 0000000000..dd50b71cb1 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetInflater.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2024 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.widget + +import android.content.Context +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherAppState +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError +import com.android.launcher3.logging.FileLog +import com.android.launcher3.model.WidgetsModel +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.qsb.QsbContainerView + +/** Utility class for handling widget inflation taking into account all the restore state updates */ +class WidgetInflater(private val context: Context) { + + private val widgetHelper = WidgetManagerHelper(context) + + fun inflateAppWidget( + item: LauncherAppWidgetInfo, + ): InflationResult { + if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) { + item.providerName = QsbContainerView.getSearchComponentName(context) + if (item.providerName == null) { + return InflationResult( + TYPE_DELETE, + reason = "search widget removed because search component cannot be found", + restoreErrorType = RestoreError.NO_SEARCH_WIDGET + ) + } + } + if (LauncherAppState.INSTANCE.get(context).isSafeModeEnabled) { + return InflationResult(TYPE_PENDING) + } + val appWidgetInfo: LauncherAppWidgetProviderInfo? + var removalReason = "" + @RestoreError var logReason = RestoreError.APP_NOT_INSTALLED + var update = false + + if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { + // The widget id is not valid. Try to find the widget based on the provider info. + appWidgetInfo = widgetHelper.findProvider(item.providerName, item.user) + if (appWidgetInfo == null) { + if (WidgetsModel.GO_DISABLE_WIDGETS) { + removalReason = "widgets are disabled on go device." + logReason = RestoreError.WIDGETS_DISABLED + } else { + removalReason = "WidgetManagerHelper cannot find a provider from provider info." + logReason = RestoreError.MISSING_WIDGET_PROVIDER + } + } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) { + // since appWidgetInfo is not null anymore, update the provider status + item.restoreStatus = + item.restoreStatus and LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY.inv() + update = true + } + } else { + appWidgetInfo = + widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId, item.targetComponent) + if (appWidgetInfo == null) { + if (item.appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) { + removalReason = "CustomWidgetManager cannot find provider from that widget id." + logReason = RestoreError.MISSING_INFO + } else { + removalReason = + ("AppWidgetManager cannot find provider for that widget id." + + " It could be because AppWidgetService is not available, or the" + + " appWidgetId has not been bound to a the provider yet, or you" + + " don't have access to that appWidgetId.") + logReason = RestoreError.INVALID_WIDGET_ID + } + } + } + + // If the provider is ready, but the widget is not yet restored, try to restore it. + if ( + !item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) && + item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED + ) { + if (appWidgetInfo == null) { + return InflationResult( + type = TYPE_DELETE, + reason = + "Removing restored widget: id=${item.appWidgetId} belongs to component ${item.providerName} user ${item.user}, as the provider is null and $removalReason", + restoreErrorType = logReason + ) + } + + // If we do not have a valid id, try to bind an id. + if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { + if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) { + // Id has not been allocated yet. Allocate a new id. + LauncherWidgetHolder.newInstance(context).let { + item.appWidgetId = it.allocateAppWidgetId() + it.destroy() + } + item.restoreStatus = + item.restoreStatus or LauncherAppWidgetInfo.FLAG_ID_ALLOCATED + + // Also try to bind the widget. If the bind fails, the user will be shown + // a click to setup UI, which will ask for the bind permission. + val pendingInfo = PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer) + pendingInfo.spanX = item.spanX + pendingInfo.spanY = item.spanY + pendingInfo.minSpanX = item.minSpanX + pendingInfo.minSpanY = item.minSpanY + var options = pendingInfo.getDefaultSizeOptions(context) + val isDirectConfig = + item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG) + if (isDirectConfig && item.bindOptions != null) { + val newOptions = item.bindOptions.extras + if (options != null) { + newOptions!!.putAll(options) + } + options = newOptions + } + val success = + widgetHelper.bindAppWidgetIdIfAllowed( + item.appWidgetId, + appWidgetInfo, + options + ) + + // We tried to bind once. If we were not able to bind, we would need to + // go through the permission dialog, which means we cannot skip the config + // activity. + item.bindOptions = null + item.restoreStatus = + item.restoreStatus and LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG.inv() + + // Bind succeeded + if (success) { + // If the widget has a configure activity, it is still needs to set it + // up, otherwise the widget is ready to go. + item.restoreStatus = + if ((appWidgetInfo.configure == null) || isDirectConfig) + LauncherAppWidgetInfo.RESTORE_COMPLETED + else LauncherAppWidgetInfo.FLAG_UI_NOT_READY + } + update = true + } + } else if ( + (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) && + (appWidgetInfo.configure == null)) + ) { + // The widget was marked as UI not ready, but there is no configure activity to + // update the UI. + item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED + update = true + } else if ( + (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) && + appWidgetInfo.configure != null) + ) { + if (widgetHelper.isAppWidgetRestored(item.appWidgetId)) { + item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED + update = true + } + } + } + + if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { + // Verify that we own the widget + if (appWidgetInfo == null) { + FileLog.e(Launcher.TAG, "Removing invalid widget: id=" + item.appWidgetId) + return InflationResult(TYPE_DELETE, reason = removalReason) + } + item.minSpanX = appWidgetInfo.minSpanX + item.minSpanY = appWidgetInfo.minSpanY + return InflationResult(TYPE_REAL, isUpdate = update, widgetInfo = appWidgetInfo) + } else { + return InflationResult(TYPE_PENDING, isUpdate = update, widgetInfo = appWidgetInfo) + } + } + + data class InflationResult( + val type: Int, + val reason: String? = null, + @RestoreError val restoreErrorType: String = RestoreError.APP_NOT_INSTALLED, + val isUpdate: Boolean = false, + val widgetInfo: LauncherAppWidgetProviderInfo? = null + ) + + companion object { + const val TYPE_DELETE = 0 + + const val TYPE_PENDING = 1 + + const val TYPE_REAL = 2 + } +} diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java index 0860e72420..52767a4135 100644 --- a/src/com/android/launcher3/widget/WidgetManagerHelper.java +++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java @@ -24,8 +24,11 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; +import android.widget.RemoteViews; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -61,8 +64,7 @@ public class WidgetManagerHelper { int appWidgetId, ComponentName componentName) { // For custom widgets. - if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID && !CustomWidgetManager - .INSTANCE.get(mContext).getWidgetIdForCustomProvider(componentName).equals("")) { + if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) { return CustomWidgetManager.INSTANCE.get(mContext).getWidgetProvider(componentName); } @@ -131,6 +133,23 @@ public class WidgetManagerHelper { appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED); } + + /** + * Load RemoteViews preview for this provider if available. + * + * @param info The provider info for the widget you want to preview. + * @param widgetCategory The widget category for which you want to display previews. + * + * @return Returns the widget preview that matches selected category, if available. + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + public RemoteViews loadGeneratedPreview(@NonNull AppWidgetProviderInfo info, + int widgetCategory) { + if (!android.appwidget.flags.Flags.generatedPreviews()) return null; + return mAppWidgetManager.getWidgetPreview(info.provider, info.getProfile(), widgetCategory); + } + private static Stream allWidgetsSteam(Context context) { AppWidgetManager awm = context.getSystemService(AppWidgetManager.class); return Stream.concat( diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index c347939d0a..ceb0072310 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -16,6 +16,7 @@ package com.android.launcher3.widget; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY; import android.content.Context; @@ -187,7 +188,13 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { mWidgetCellHorizontalPadding) .forEach(row -> { TableRow tableRow = new TableRow(getContext()); - tableRow.setGravity(Gravity.TOP); + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, + // they can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } row.forEach(widgetItem -> { WidgetCell widget = addItemCell(tableRow); widget.applyFromCellItem(widgetItem); diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java index 44571a6c4a..398b1df394 100644 --- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java @@ -33,13 +33,9 @@ import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo implements Parcelable { - public final String providerId; - - protected CustomAppWidgetProviderInfo(Parcel parcel, boolean readSelf, String providerId) { + protected CustomAppWidgetProviderInfo(Parcel parcel, boolean readSelf) { super(parcel); if (readSelf) { - this.providerId = parcel.readString(); - provider = new ComponentName(parcel.readString(), CLS_CUSTOM_WIDGET_PREFIX + parcel.readString()); @@ -53,8 +49,6 @@ public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo spanY = parcel.readInt(); minSpanX = parcel.readInt(); minSpanY = parcel.readInt(); - } else { - this.providerId = providerId; } } @@ -77,7 +71,6 @@ public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); - out.writeString(providerId); out.writeString(provider.getPackageName()); out.writeString(provider.getClassName()); @@ -93,12 +86,12 @@ public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo out.writeInt(minSpanY); } - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator<>() { @Override public CustomAppWidgetProviderInfo createFromParcel(Parcel parcel) { - return new CustomAppWidgetProviderInfo(parcel, true, ""); + return new CustomAppWidgetProviderInfo(parcel, true); } @Override diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java index 7cf022175b..2fdf35429d 100644 --- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java +++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java @@ -18,6 +18,7 @@ package com.android.launcher3.widget.custom; import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET; import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID; +import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; @@ -44,7 +45,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; @@ -57,17 +57,16 @@ public class CustomWidgetManager implements PluginListener, new MainThreadInitializedObject<>(CustomWidgetManager::new); private static final String TAG = "CustomWidgetManager"; + private static final String PLUGIN_PKG = "android"; private final Context mContext; - private final HashMap mPlugins; + private final HashMap mPlugins; private final List mCustomWidgets; - private final HashMap mWidgetsIdMap; private Consumer mWidgetRefreshCallback; private CustomWidgetManager(Context context) { mContext = context; mPlugins = new HashMap<>(); mCustomWidgets = new ArrayList<>(); - mWidgetsIdMap = new HashMap<>(); PluginManagerWrapper.INSTANCE.get(context) .addPluginListener(this, CustomWidgetPlugin.class, true); @@ -78,8 +77,7 @@ public class CustomWidgetManager implements PluginListener, Class cls = Class.forName(s); CustomWidgetPlugin plugin = (CustomWidgetPlugin) cls.getDeclaredConstructor(Context.class).newInstance(context); - mPlugins.put(plugin.getId(), plugin); - onPluginConnected(mPlugins.get(plugin.getId()), context); + onPluginConnected(plugin, context); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException | NoSuchMethodException | InvocationTargetException e) { @@ -102,35 +100,17 @@ public class CustomWidgetManager implements PluginListener, Parcel parcel = Parcel.obtain(); providers.get(0).writeToParcel(parcel, 0); parcel.setDataPosition(0); - CustomAppWidgetProviderInfo info = newInfo(plugin.getId(), plugin, parcel, context); + CustomAppWidgetProviderInfo info = newInfo(plugin, parcel); parcel.recycle(); + mPlugins.put(info.provider, plugin); mCustomWidgets.add(info); - mWidgetsIdMap.put(info.provider, plugin.getId()); } @Override public void onPluginDisconnected(CustomWidgetPlugin plugin) { - String providerId = plugin.getId(); - if (mPlugins.containsKey(providerId)) { - mPlugins.remove(providerId); - } - - ComponentName cn = null; - for (Map.Entry entry: mWidgetsIdMap.entrySet()) { - if (entry.getValue().equals(providerId)) { - cn = (ComponentName) entry.getKey(); - } - } - - if (cn != null) { - mWidgetsIdMap.remove(cn); - for (int i = 0; i < mCustomWidgets.size(); i++) { - if (mCustomWidgets.get(i).getComponent().equals(cn)) { - mCustomWidgets.remove(i); - return; - } - } - } + ComponentName cn = getWidgetProviderComponent(plugin); + mPlugins.remove(cn); + mCustomWidgets.removeIf(w -> w.getComponent().equals(cn)); } /** @@ -145,7 +125,7 @@ public class CustomWidgetManager implements PluginListener, */ public void onViewCreated(LauncherAppWidgetHostView view) { CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo(); - CustomWidgetPlugin plugin = mPlugins.get(info.providerId); + CustomWidgetPlugin plugin = mPlugins.get(info.provider); if (plugin == null) return; plugin.onViewCreated(view); } @@ -158,33 +138,19 @@ public class CustomWidgetManager implements PluginListener, return mCustomWidgets.stream(); } - /** - * Returns the widget id for a specific provider. - */ - public String getWidgetIdForCustomProvider(@NonNull ComponentName provider) { - if (mWidgetsIdMap.containsKey(provider)) { - return mWidgetsIdMap.get(provider); - } else { - return ""; - } - } - /** * Returns the widget provider in respect to given widget id. */ @Nullable - public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName componentName) { - for (LauncherAppWidgetProviderInfo info : mCustomWidgets) { - if (info.provider.equals(componentName)) return info; - } - return null; + public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName cn) { + return mCustomWidgets.stream() + .filter(w -> w.getComponent().equals(cn)).findAny().orElse(null); } - private static CustomAppWidgetProviderInfo newInfo(String providerId, CustomWidgetPlugin plugin, - Parcel parcel, Context context) { - CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo( - parcel, false, providerId); - plugin.updateWidgetInfo(info, context); + private CustomAppWidgetProviderInfo newInfo(CustomWidgetPlugin plugin, Parcel parcel) { + CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false); + info.provider = getWidgetProviderComponent(plugin); + plugin.updateWidgetInfo(info, mContext); return info; } @@ -195,4 +161,8 @@ public class CustomWidgetManager implements PluginListener, return CUSTOM_WIDGET_ID - mCustomWidgets.indexOf(getWidgetProvider(componentName)); } + private ComponentName getWidgetProviderComponent(CustomWidgetPlugin plugin) { + return new ComponentName( + PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName()); + } } diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java new file mode 100644 index 0000000000..072d1d5948 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.widget.picker; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import java.util.Objects; + +/** + * A category of widget recommendations displayed in the widget picker (launched from "Widgets" + * option in the pop-up opened on long press of launcher workspace). + */ +public class WidgetRecommendationCategory implements Comparable { + /** Resource id that holds the user friendly label for the category. */ + @StringRes + public final int categoryTitleRes; + /** + * Relative order of this category with respect to other categories. + * + *

Category with lowest order is displayed first in the recommendations section.

+ */ + public final int order; + + public WidgetRecommendationCategory(@StringRes int categoryTitleRes, int order) { + this.categoryTitleRes = categoryTitleRes; + this.order = order; + } + + @Override + public int hashCode() { + return Objects.hash(categoryTitleRes, order); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof WidgetRecommendationCategory category)) { + return false; + } + return categoryTitleRes == category.categoryTitleRes + && order == category.order; + } + + @Override + public int compareTo(WidgetRecommendationCategory widgetRecommendationCategory) { + return order - widgetRecommendationCategory.order; + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java new file mode 100644 index 0000000000..801b1f662f --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 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.widget.picker; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import com.android.launcher3.R; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.util.Preconditions; +import com.android.launcher3.util.ResourceBasedOverride; + +/** + * A {@link ResourceBasedOverride} that categorizes widget recommendations. + * + *

Override the {@code widget_recommendation_category_provider_class} resource to provide your + * own implementation. Method {@code getWidgetRecommendationCategory} is called per widget to get + * the category.

+ */ +public class WidgetRecommendationCategoryProvider implements ResourceBasedOverride { + private static final String TAG = "WidgetRecommendationCategoryProvider"; + + /** + * Retrieve instance of this object that can be overridden in runtime based on the build + * variant of the application. + */ + public static WidgetRecommendationCategoryProvider newInstance(Context context) { + Preconditions.assertWorkerThread(); + return Overrides.getObject( + WidgetRecommendationCategoryProvider.class, context.getApplicationContext(), + R.string.widget_recommendation_category_provider_class); + } + + /** + * Returns a {@link WidgetRecommendationCategory} for the provided widget item that can be used + * to display the recommendation grouped by categories. + */ + @WorkerThread + public WidgetRecommendationCategory getWidgetRecommendationCategory(Context context, + WidgetItem item) { + // This is a default implementation that uses application category to derive the category to + // be displayed. The implementation can be overridden in individual launcher customization + // via the overridden WidgetRecommendationCategoryProvider resource. + + Preconditions.assertWorkerThread(); + PackageManager pm = context.getPackageManager(); + if (item.widgetInfo != null && item.widgetInfo.getComponent() != null) { + String widgetComponentName = item.widgetInfo.getComponent().getClassName(); + try { + int predictionCategory = pm.getApplicationInfo( + item.widgetInfo.getComponent().getPackageName(), 0 /* flags */).category; + return getCategoryFromApplicationCategory(context, predictionCategory, + widgetComponentName); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to retrieve application category when determining the " + + "widget category for " + widgetComponentName, e); + } + } + return null; + } + + /** Maps application category to an appropriate displayable category. */ + private static WidgetRecommendationCategory getCategoryFromApplicationCategory( + Context context, int applicationCategory, String componentName) { + // Weather categories don't map to a specific application category, so, we maintain an + // allowlist. + String[] weatherRecommendationAllowlist = + context.getResources().getStringArray(R.array.weather_recommendations); + for (String allowedWeatherComponentName : weatherRecommendationAllowlist) { + if (componentName.equalsIgnoreCase(allowedWeatherComponentName)) { + return new WidgetRecommendationCategory( + R.string.weather_widget_recommendation_category_label, /*order=*/3); + } + } + + // Fitness categories don't map to a specific application category, so, we maintain an + // allowlist. + String[] fitnessRecommendationAllowlist = + context.getResources().getStringArray(R.array.fitness_recommendations); + for (String allowedFitnessComponentName : fitnessRecommendationAllowlist) { + if (componentName.equalsIgnoreCase(allowedFitnessComponentName)) { + return new WidgetRecommendationCategory( + R.string.fitness_widget_recommendation_category_label, /*order=*/2); + } + } + + if (applicationCategory == ApplicationInfo.CATEGORY_PRODUCTIVITY) { + return new WidgetRecommendationCategory( + R.string.productivity_widget_recommendation_category_label, /*order=*/0); + } + + if (applicationCategory == ApplicationInfo.CATEGORY_NEWS) { + return new WidgetRecommendationCategory( + R.string.news_widget_recommendation_category_label, /*order=*/1); + } + + if (applicationCategory == ApplicationInfo.CATEGORY_SOCIAL + || applicationCategory == ApplicationInfo.CATEGORY_AUDIO + || applicationCategory == ApplicationInfo.CATEGORY_VIDEO + || applicationCategory == ApplicationInfo.CATEGORY_IMAGE) { + return new WidgetRecommendationCategory( + R.string.social_and_entertainment_widget_recommendation_category_label, + /*order=*/4); + } + + return new WidgetRecommendationCategory( + R.string.others_widget_recommendation_category_label, /*order=*/5); + } + +} diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java new file mode 100644 index 0000000000..426a3aeb33 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2024 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.widget.picker; + +import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.pageindicators.PageIndicatorDots; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A {@link PagedView} that displays widget recommendations in categories with dots as paged + * indicators. + */ +public final class WidgetRecommendationsView extends PagedView { + private @Px float mAvailableHeight = Float.MAX_VALUE; + + private static final int MAX_CATEGORIES = 3; + private TextView mRecommendationPageTitle; + private final List mCategoryTitles = new ArrayList<>(); + + @Nullable + private OnLongClickListener mWidgetCellOnLongClickListener; + @Nullable + private OnClickListener mWidgetCellOnClickListener; + + public WidgetRecommendationsView(Context context) { + this(context, /* attrs= */ null); + } + + public WidgetRecommendationsView(Context context, AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public WidgetRecommendationsView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void initParentViews(View parent) { + super.initParentViews(parent); + mRecommendationPageTitle = parent.findViewById(R.id.recommendations_page_title); + } + + /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */ + public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) { + mWidgetCellOnLongClickListener = onLongClickListener; + } + + /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */ + public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) { + mWidgetCellOnClickListener = widgetCellOnClickListener; + } + + /** + * Displays all the provided recommendations in a single table if they fit. + * + * @param recommendedWidgets list of widgets to be displayed in recommendation section. + * @param deviceProfile the current {@link DeviceProfile} + * @param availableHeight height in px that can be used to display the recommendations; + * recommendations that don't fit in this height won't be shown + * @param availableWidth width in px that the recommendations should display in + * @param cellPadding padding in px that should be applied to each widget in the + * recommendations + * @return {@code false} if no recommendations could fit in the available space. + */ + public boolean setRecommendations( + List recommendedWidgets, DeviceProfile deviceProfile, + final @Px float availableHeight, final @Px int availableWidth, + final @Px int cellPadding) { + this.mAvailableHeight = availableHeight; + removeAllViews(); + + maybeDisplayInTable(recommendedWidgets, deviceProfile, availableWidth, cellPadding); + updateTitleAndIndicator(); + return getChildCount() > 0; + } + + /** + * Displays the recommendations grouped by categories as pages. + *

In case of a single category, no title is displayed for it.

+ * + * @param recommendations a map of widget items per recommendation category + * @param deviceProfile the current {@link DeviceProfile} + * @param availableHeight height in px that can be used to display the recommendations; + * recommendations that don't fit in this height won't be shown + * @param availableWidth width in px that the recommendations should display in + * @param cellPadding padding in px that should be applied to each widget in the + * recommendations + * @return {@code false} if no recommendations could fit in the available space. + */ + public boolean setRecommendations( + Map> recommendations, + DeviceProfile deviceProfile, + final @Px float availableHeight, final @Px int availableWidth, + final @Px int cellPadding) { + this.mAvailableHeight = availableHeight; + Context context = getContext(); + mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels); + removeAllViews(); + + int displayedCategories = 0; + + // Render top MAX_CATEGORIES in separate tables. Each table becomes a page. + for (Map.Entry> entry : + new TreeMap<>(recommendations).entrySet()) { + // If none of the recommendations for the category could fit in the mAvailableHeight, we + // don't want to add that category; and we look for the next one. + if (maybeDisplayInTable(entry.getValue(), deviceProfile, availableWidth, cellPadding)) { + mCategoryTitles.add( + context.getResources().getString(entry.getKey().categoryTitleRes)); + displayedCategories++; + } + + if (displayedCategories == MAX_CATEGORIES) { + break; + } + } + + updateTitleAndIndicator(); + mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels); + return getChildCount() > 0; + } + + /** Displays the page title and paging indicator if there are multiple pages. */ + private void updateTitleAndIndicator() { + boolean showPaginatedView = getPageCount() > 1; + int titleAndIndicatorVisibility = showPaginatedView ? View.VISIBLE : View.GONE; + mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility); + mPageIndicator.setVisibility(titleAndIndicatorVisibility); + if (showPaginatedView) { + mPageIndicator.setActiveMarker(0); + setCurrentPage(0); + mRecommendationPageTitle.setText(mCategoryTitles.get(0)); + } + } + + @Override + protected void notifyPageSwitchListener(int prevPage) { + if (getPageCount() > 1) { + // Since the title is outside the paging scroll, we update the title on page switch. + mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage())); + super.notifyPageSwitchListener(prevPage); + requestLayout(); + } + } + + @Override + protected boolean canScroll(float absVScroll, float absHScroll) { + // Allow only horizontal scroll. + return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + mPageIndicator.setScroll(l, mMaxScroll); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + boolean hasMultiplePages = getChildCount() > 0; + + if (hasMultiplePages) { + int finalWidth = MeasureSpec.getSize(widthMeasureSpec); + int desiredHeight = 0; + + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + measureChild(child, widthMeasureSpec, heightMeasureSpec); + if (mAvailableHeight == Float.MAX_VALUE) { + // When we are not limited by height, use currentPage's height. This is the case + // when the paged layout is placed in a scrollable container. We cannot use + // height + // of tallest child in such case, as it will display a scrollbar even for + // smaller + // pages that don't have more content. + if (i == mCurrentPage) { + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + desiredHeight = Math.max(parentHeight, child.getMeasuredHeight()); + } + } else { + // Use height of tallest child when we are limited to a certain height. + desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight()); + } + } + + int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0); + setMeasuredDimension(finalWidth, finalHeight); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + /** + * Groups the provided recommendations into rows and displays them in a table if at least one + * fits. + *

Returns false if none of the recommendations could fit.

+ */ + private boolean maybeDisplayInTable(List recommendedWidgets, + DeviceProfile deviceProfile, + final @Px int availableWidth, final @Px int cellPadding) { + Context context = getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + List> rows = groupWidgetItemsUsingRowPxWithoutReordering( + recommendedWidgets, + context, + deviceProfile, + availableWidth, + cellPadding); + + WidgetsRecommendationTableLayout recommendationsTable = + (WidgetsRecommendationTableLayout) inflater.inflate( + R.layout.widget_recommendations_table, + /* root=*/ this, + /* attachToRoot=*/ false); + recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener); + recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener); + + boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows, + deviceProfile, mAvailableHeight); + if (displayedAtLeastOne) { + addView(recommendationsTable); + } + + return displayedAtLeastOne; + } + + /** Returns location of a widget cell for displaying the "touch and hold" education tip. */ + public View getViewForEducationTip() { + if (getChildCount() > 0) { + // first page (a table layout) -> first item (a widget cell). + return ((ViewGroup) getChildAt(0)).getChildAt(0); + } + return null; + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index e9a590b814..f5742af9f6 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -17,18 +17,20 @@ package com.android.launcher3.widget.picker; import static android.view.View.MeasureSpec.makeMeasureSpec; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_DIALOG_SEEN; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; import android.animation.Animator; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.os.Build; +import android.os.Parcelable; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; @@ -44,6 +46,7 @@ import android.view.WindowInsetsController; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Button; +import android.widget.LinearLayout; import android.widget.TextView; import android.window.BackEvent; @@ -64,7 +67,6 @@ 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; @@ -75,7 +77,6 @@ import com.android.launcher3.widget.BaseWidgetSheet; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.search.SearchModeListener; import com.android.launcher3.widget.picker.search.WidgetsSearchBar; -import com.android.launcher3.widget.util.WidgetsTableUtils; import com.android.launcher3.workprofile.PersonalWorkPagedView; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; @@ -99,7 +100,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet // resolution or landscape on phone. This ratio defines the max percentage of content area that // the table can display. private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f; - private final UserCache mUserCache; private final UserManagerState mUserManagerState = new UserManagerState(); private final UserHandle mCurrentUser = Process.myUserHandle(); @@ -162,17 +162,19 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Nullable PersonalWorkPagedView mViewPager; private boolean mIsInSearchMode; private boolean mIsNoWidgetsViewNeeded; - @Px private int mMaxSpanPerRow; + @Px protected int mMaxSpanPerRow; protected DeviceProfile mDeviceProfile; protected TextView mNoWidgetsView; protected StickyHeaderLayout mSearchScrollView; - protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable; + protected WidgetRecommendationsView mWidgetRecommendationsView; + protected LinearLayout mWidgetRecommendationsContainer; protected View mTabBar; protected View mSearchBarContainer; protected WidgetsSearchBar mSearchBar; protected TextView mHeaderTitle; protected RecyclerViewFastScroller mFastScroller; + protected int mBottomPadding; public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); @@ -182,7 +184,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet .stream() .anyMatch(user -> mUserCache.getUserInfo(user).isWork()); mWorkWidgetsFilter = entry -> mHasWorkProfile - && mUserCache.getUserInfo(entry.mPkgItem.user).isWork(); + && mUserCache.getUserInfo(entry.mPkgItem.user).isWork() + && !mUserManagerState.isUserQuiet(entry.mPkgItem.user); mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); @@ -219,9 +222,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet setupViews(); - mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table); - mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); - mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); + mWidgetRecommendationsContainer = mSearchScrollView.findViewById( + R.id.widget_recommendations_container); + mWidgetRecommendationsView = mSearchScrollView.findViewById( + R.id.widget_recommendations_view); + mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); + mWidgetRecommendationsView.setWidgetCellLongClickListener(this); + mWidgetRecommendationsView.setWidgetCellOnClickListener(this); + mHeaderTitle = mSearchScrollView.findViewById(R.id.title); onRecommendedWidgetsBound(); @@ -369,15 +377,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override public void setInsets(Rect insets) { super.setInsets(insets); - int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); - setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, bottomPadding); - setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, bottomPadding); + mBottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); + setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, mBottomPadding); + setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, mBottomPadding); if (mHasWorkProfile) { - setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, bottomPadding); + setBottomPadding(mAdapters.get(AdapterHolder.WORK) + .mWidgetsRecyclerView, mBottomPadding); } - ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = bottomPadding; + ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = mBottomPadding; - if (bottomPadding > 0) { + if (mBottomPadding > 0) { setupNavBarColor(); } else { clearNavBarColor(); @@ -386,6 +395,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet requestLayout(); } + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + WindowInsets w = super.onApplyWindowInsets(insets); + if (mInsets.bottom != mNavBarScrimHeight) { + setInsets(mInsets); + } + return w; + } + private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { recyclerView.setPadding( recyclerView.getPaddingLeft(), @@ -510,11 +528,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet } @Override - public void enterSearchMode() { + public void enterSearchMode(boolean shouldLog) { if (mIsInSearchMode) return; setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); - mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); + if (shouldLog) { + mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); + } } @Override @@ -537,7 +557,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { mIsInSearchMode = isInSearchMode; if (isInSearchMode) { - mRecommendedWidgetsTable.setVisibility(GONE); + mWidgetRecommendationsContainer.setVisibility(GONE); if (mHasWorkProfile) { mViewPager.setVisibility(GONE); mTabBar.setVisibility(GONE); @@ -566,40 +586,46 @@ public class WidgetsFullSheet extends BaseWidgetSheet if (mIsInSearchMode) { return; } - List recommendedWidgets = - mActivityContext.getPopupDataProvider().getRecommendedWidgets(); - mHasRecommendedWidgets = recommendedWidgets.size() > 0; - if (mHasRecommendedWidgets) { - float noWidgetsViewHeight = 0; - if (mIsNoWidgetsViewNeeded) { - // Make sure recommended section leaves enough space for noWidgetsView. - Rect noWidgetsViewTextBounds = new Rect(); - mNoWidgetsView.getPaint() - .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0, - mNoWidgetsView.getText().length(), noWidgetsViewTextBounds); - noWidgetsViewHeight = noWidgetsViewTextBounds.height(); - } - if (!isTwoPane()) { - doMeasure( - makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, - MeasureSpec.EXACTLY), - makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, - MeasureSpec.EXACTLY)); - } - float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight); - List> recommendedWidgetsInTable = - WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering( - recommendedWidgets, - mActivityContext, - mActivityContext.getDeviceProfile(), - mMaxSpanPerRow, - mWidgetCellHorizontalPadding); - mRecommendedWidgetsTable.setRecommendedWidgets( - recommendedWidgetsInTable, maxTableHeight); + if (enableCategorizedWidgetSuggestions()) { + mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(), + mDeviceProfile, + /* availableHeight= */ getMaxAvailableHeightForRecommendations(), + /* availableWidth= */ mMaxSpanPerRow, + /* cellPadding= */ mWidgetCellHorizontalPadding + ); } else { - mRecommendedWidgetsTable.setVisibility(GONE); + mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mActivityContext.getPopupDataProvider().getRecommendedWidgets(), + mDeviceProfile, + /* availableHeight= */ getMaxAvailableHeightForRecommendations(), + /* availableWidth= */ mMaxSpanPerRow, + /* cellPadding= */ mWidgetCellHorizontalPadding + ); } + mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE); + } + + @Px + private float getMaxAvailableHeightForRecommendations() { + float noWidgetsViewHeight = 0; + if (mIsNoWidgetsViewNeeded) { + // Make sure recommended section leaves enough space for noWidgetsView. + Rect noWidgetsViewTextBounds = new Rect(); + mNoWidgetsView.getPaint() + .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0, + mNoWidgetsView.getText().length(), noWidgetsViewTextBounds); + noWidgetsViewHeight = noWidgetsViewTextBounds.height(); + } + if (!isTwoPane()) { + doMeasure( + makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, + MeasureSpec.EXACTLY), + makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, + MeasureSpec.EXACTLY)); + } + return getMaxTableHeight(noWidgetsViewHeight); } /** b/209579563: "Widgets" header should be focused first. */ @@ -608,7 +634,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet return mHeaderTitle; } - protected float getMaxTableHeight(float noWidgetsViewHeight) { + @Px + protected float getMaxTableHeight(@Px float noWidgetsViewHeight) { return (mContent.getMeasuredHeight() - mTabsHeight - getHeaderViewHeight() - noWidgetsViewHeight) @@ -685,7 +712,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet private static int getWidgetSheetId(BaseActivity activity) { boolean isTwoPane = (activity.getDeviceProfile().isTablet - && activity.getDeviceProfile().isLandscape + // Enables two pane picker for tablets in all orientations when the + // enableCategorizedWidgetSuggestions flag is on. + && (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions()) && !activity.getDeviceProfile().isTwoPanels) // Enables two pane picker for unfolded foldables if the flag is on. || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker()); @@ -777,33 +806,62 @@ public class WidgetsFullSheet extends BaseWidgetSheet + marginLayoutParams.topMargin; } - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + private int getCurrentAdapterHolderType() { if (mIsInSearchMode) { - mSearchBar.reset(); + return SEARCH; + } else if (mViewPager != null) { + return mViewPager.getCurrentPage(); + } else { + return AdapterHolder.PRIMARY; + } + } + + private void restorePreviousAdapterHolderType(int previousAdapterHolderType) { + if (previousAdapterHolderType == AdapterHolder.WORK && mViewPager != null) { + mViewPager.setCurrentPage(previousAdapterHolderType); + } else if (previousAdapterHolderType == AdapterHolder.SEARCH) { + enterSearchMode(false); } } @Override public void onDeviceProfileChanged(DeviceProfile dp) { - if (mDeviceProfile.isLandscape != dp.isLandscape && dp.isTablet && !dp.isTwoPanels) { - handleClose(false); - show(BaseActivity.fromContext(getContext()), false); - } else { - reset(); - } + super.onDeviceProfileChanged(dp); - // When folding/unfolding the foldables, we need to switch between the regular widget picker - // and the two pane picker, so we rebuild the picker with the correct layout. - if (mDeviceProfile.isTwoPanels != dp.isTwoPanels && enableUnfoldedTwoPanePicker()) { + if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) { + SparseArray widgetsState = new SparseArray<>(); + saveHierarchyState(widgetsState); handleClose(false); - show(BaseActivity.fromContext(getContext()), false); + WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false); + sheet.restoreHierarchyState(widgetsState); + sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType()); + } else if (!isTwoPane()) { + reset(); + resetExpandedHeaders(); } mDeviceProfile = dp; } + /** + * Indicates if layout should be re-created on device profile change - so that a different + * layout can be displayed. + */ + private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) { + // When folding/unfolding the foldables, we need to switch between the regular widget picker + // and the two pane picker, so we rebuild the picker with the correct layout. + boolean isFoldUnFold = + oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker(); + // In tablets, on orientation change we switch between single and two pane picker unless the + // categorized suggestions flag was on. With the categorized suggestions feature, we use a + // two pane picker across all orientations. + boolean useDifferentLayoutOnOrientationChange = + (!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels + && oldDp.isLandscape != newDp.isLandscape)); + + return isFoldUnFold || useDifferentLayoutOnOrientationChange; + } + @Override public void onBackInvoked() { if (mIsInSearchMode) { @@ -824,9 +882,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet } @Nullable private View getViewToShowEducationTip() { - if (mRecommendedWidgetsTable.getVisibility() == VISIBLE - && mRecommendedWidgetsTable.getChildCount() > 0) { - return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0); + if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) { + return mWidgetRecommendationsView.getViewForEducationTip(); } AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java index b5e7401e10..d373a3b327 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java @@ -35,9 +35,9 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.icons.PlaceHolderIconDrawable; -import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.util.CancellableTask; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; @@ -52,8 +52,12 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd private static final int[] EXPANDED_DRAWABLE_STATE = new int[] {android.R.attr.state_expanded}; private final int mIconSize; - - @Nullable private HandlerRunnable mIconLoadRequest; + /** + * Indicates if the header is collapsable. For example, when displayed in a two pane layout, + * widget apps aren't collapsable. + */ + private final boolean mIsCollapsable; + @Nullable private CancellableTask mIconLoadRequest; @Nullable private Drawable mIconDrawable; @Nullable private WidgetsListDrawableState mListDrawableState; private ImageView mAppIcon; @@ -79,6 +83,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0); mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize, grid.iconSizePx); + mIsCollapsable = a.getBoolean(R.styleable.WidgetsListRowHeader_collapsable, true); } @Override @@ -87,32 +92,36 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd mAppIcon = findViewById(R.id.app_icon); mTitle = findViewById(R.id.app_title); mSubtitle = findViewById(R.id.app_subtitle); - setAccessibilityDelegate(new AccessibilityDelegate() { + // Lists that cannot collapse, don't need EXPAND or COLLAPSE accessibility actions. + if (mIsCollapsable) { + setAccessibilityDelegate(new AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - if (mIsExpanded) { - info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND); - info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); - } else { - info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE); - info.addAction(AccessibilityNodeInfo.ACTION_EXPAND); + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + if (mIsExpanded) { + info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND); + info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); + } else { + info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE); + info.addAction(AccessibilityNodeInfo.ACTION_EXPAND); + } + super.onInitializeAccessibilityNodeInfo(host, info); } - super.onInitializeAccessibilityNodeInfo(host, info); - } - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - switch (action) { - case AccessibilityNodeInfo.ACTION_EXPAND: - case AccessibilityNodeInfo.ACTION_COLLAPSE: - callOnClick(); - return true; - default: - return super.performAccessibilityAction(host, action, args); + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + switch (action) { + case AccessibilityNodeInfo.ACTION_EXPAND: + case AccessibilityNodeInfo.ACTION_COLLAPSE: + callOnClick(); + return true; + default: + return super.performAccessibilityAction(host, action, args); + } } - } - }); + }); + } } /** Sets the expand toggle to expand / collapse. */ diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index c7d2aa3fd8..ef3ccf0f5b 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; + import android.content.Context; import android.graphics.Bitmap; import android.util.Log; @@ -122,6 +124,7 @@ public final class WidgetsListTableViewHolderBinder widget.applyFromCellItem(widgetItem, 1f, bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)), holder.previewCache.get(widgetItem)); + widget.requestLayout(); } } } @@ -147,7 +150,13 @@ public final class WidgetsListTableViewHolderBinder tableRow = (TableRow) table.getChildAt(i); } else { tableRow = new TableRow(table.getContext()); - tableRow.setGravity(Gravity.TOP); + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, they + // can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } table.addView(tableRow); } if (tableRow.getChildCount() > widgetItems.size()) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 06cc65e4c1..12564f4932 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import android.content.Context; @@ -31,7 +32,6 @@ import android.widget.TableRow; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.widget.WidgetCell; @@ -61,7 +61,7 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { super(context, attrs); // There are 1 row for title, 1 row for dimension and 2 rows for description. mWidgetsRecommendationTableVerticalPadding = 2 * getResources() - .getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding); + .getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding); mWidgetCellVerticalPadding = 2 * getResources() .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding); mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size); @@ -84,17 +84,22 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { *

If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only * row still doesn't fit, we scale down the preview image. + * + *

Returns {@code false} if none of the widgets could fit

*/ - public void setRecommendedWidgets(List> recommendedWidgets, + public boolean setRecommendedWidgets(List> recommendedWidgets, + DeviceProfile deviceProfile, float recommendationTableMaxHeight) { mRecommendationTableMaxHeight = recommendationTableMaxHeight; RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f, + deviceProfile, recommendedWidgets); bindData(data); + return !data.mRecommendationTable.isEmpty(); } private void bindData(RecommendationTableData data) { - if (data.mRecommendationTable.size() == 0) { + if (data.mRecommendationTable.isEmpty()) { setVisibility(GONE); return; } @@ -104,12 +109,21 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { for (int i = 0; i < data.mRecommendationTable.size(); i++) { List widgetItems = data.mRecommendationTable.get(i); TableRow tableRow = new TableRow(getContext()); - tableRow.setGravity(Gravity.TOP); - + if (enableCategorizedWidgetSuggestions()) { + // Vertically center align items, so that even if they don't fill bounds, they can + // look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); + } else { + tableRow.setGravity(Gravity.TOP); + } for (WidgetItem widgetItem : widgetItems) { WidgetCell widgetCell = addItemCell(tableRow); widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale); + widgetCell.showAppIconInWidgetTitle(true); widgetCell.showBadge(); + if (enableCategorizedWidgetSuggestions()) { + widgetCell.showDescription(false); + } } addView(tableRow); } @@ -132,6 +146,7 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { private RecommendationTableData fitRecommendedWidgetsToTableSpace( float previewScale, + DeviceProfile deviceProfile, List> recommendedWidgetsInTable) { if (previewScale < MAX_DOWN_SCALE_RATIO) { Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale); @@ -139,7 +154,6 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { } // A naive estimation of the widgets recommendation table height without inflation. float totalHeight = mWidgetsRecommendationTableVerticalPadding; - DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile(); for (int i = 0; i < recommendedWidgetsInTable.size(); i++) { List widgetItems = recommendedWidgetsInTable.get(i); float rowHeight = 0; @@ -163,12 +177,14 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { // num of row by 1 to see if it fits. return fitRecommendedWidgetsToTableSpace( previewScale, + deviceProfile, recommendedWidgetsInTable.subList(/* fromIndex= */0, /* toIndex= */recommendedWidgetsInTable.size() - 1)); } float nextPreviewScale = previewScale * DOWN_SCALE_RATIO; - return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable); + return fitRecommendedWidgetsToTableSpace(nextPreviewScale, deviceProfile, + recommendedWidgetsInTable); } /** Data class for the widgets recommendation table and widgets preview scaling. */ diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index c3ab08c153..165b2feb62 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -15,10 +15,12 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; import android.content.Context; import android.graphics.Outline; +import android.graphics.Rect; import android.os.Process; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -55,13 +57,15 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395; private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry"; - private LinearLayout mSuggestedWidgetsContainer; + private FrameLayout mSuggestedWidgetsContainer; private WidgetsListHeader mSuggestedWidgetsHeader; + private PackageUserKey mSuggestedWidgetsPackageUserKey; private LinearLayout mRightPane; private ScrollView mRightPaneScrollView; private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder; private int mActivePage = -1; + private PackageUserKey mSelectedHeader; private final ViewOutlineProvider mViewOutlineProviderRightPane = new ViewOutlineProvider() { @Override @@ -106,9 +110,15 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this); - mRecommendedWidgetsTable = mContent.findViewById(R.id.recommended_widget_table); - mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); - mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); + + mWidgetRecommendationsContainer = mContent.findViewById( + R.id.widget_recommendations_container); + mWidgetRecommendationsView = mContent.findViewById( + R.id.widget_recommendations_view); + mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); + mWidgetRecommendationsView.setWidgetCellLongClickListener(this); + mWidgetRecommendationsView.setWidgetCellOnClickListener(this); + mHeaderTitle = mContent.findViewById(R.id.title); mRightPane = mContent.findViewById(R.id.right_pane); mRightPane.setOutlineProvider(mViewOutlineProviderRightPane); @@ -143,6 +153,23 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } layoutParams.weight = layoutParams.width == 0 ? 0.33F : 0; leftPane.setLayoutParams(layoutParams); + requestApplyInsets(); + if (mSelectedHeader != null) { + if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) { + mSuggestedWidgetsHeader.callOnClick(); + } else { + getHeaderChangeListener().onHeaderChanged(mSelectedHeader); + } + } + } + } + + @Override + public void onWidgetsBound() { + super.onWidgetsBound(); + if (!mHasRecommendedWidgets && mSelectedHeader == null) { + mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); + mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop(); } } @@ -175,10 +202,14 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { return false; } }; - packageItemInfo.title = getContext().getString(R.string.suggested_widgets_header_title); + String suggestionsHeaderTitle = getContext().getString( + R.string.suggested_widgets_header_title); + String suggestionsRightPaneTitle = getContext().getString( + R.string.widget_picker_right_pane_accessibility_title, suggestionsHeaderTitle); + packageItemInfo.title = suggestionsHeaderTitle; WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create( packageItemInfo, - getContext().getString(R.string.suggested_widgets_header_title), + suggestionsHeaderTitle, mActivityContext.getPopupDataProvider().getRecommendedWidgets()) .withWidgetListShown(); @@ -189,14 +220,19 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mSuggestedWidgetsHeader.setExpanded(true); resetExpandedHeaders(); mRightPane.removeAllViews(); - mRightPane.addView(mRecommendedWidgetsTable); + mRightPane.addView(mWidgetRecommendationsContainer); mRightPaneScrollView.setScrollY(0); + mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); + mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo); + mSelectedHeader = mSuggestedWidgetsPackageUserKey; }); mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader); + mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); } @Override - protected float getMaxTableHeight(float noWidgetsViewHeight) { + @Px + protected float getMaxTableHeight(@Px float noWidgetsViewHeight) { return Float.MAX_VALUE; } @@ -269,6 +305,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { return new HeaderChangeListener() { @Override public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { + mSelectedHeader = selectedHeader; WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider() .getSelectedAppWidgets(selectedHeader); @@ -279,25 +316,57 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { if (mSuggestedWidgetsHeader != null) { mSuggestedWidgetsHeader.setExpanded(false); } + + WidgetsListContentEntry contentEntryToBind; + if (enableCategorizedWidgetSuggestions()) { + // Setting max span size enables row to understand how to fit more than one item + // in a row. + contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow); + } else { + contentEntryToBind = contentEntry; + } + WidgetsRowViewHolder widgetsRowViewHolder = mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane); mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, - contentEntry, + contentEntryToBind, ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, Collections.EMPTY_LIST); widgetsRowViewHolder.mDataCallback = data -> { mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, - contentEntry, + contentEntryToBind, ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, Collections.singletonList(data)); }; mRightPane.removeAllViews(); mRightPane.addView(widgetsRowViewHolder.itemView); mRightPaneScrollView.setScrollY(0); + mRightPane.setAccessibilityPaneTitle( + getContext().getString( + R.string.widget_picker_right_pane_accessibility_title, + contentEntry.mPkgItem.title)); } }; } + @Override + public void setInsets(Rect insets) { + super.setInsets(insets); + FrameLayout rightPaneContainer = mContent.findViewById(R.id.right_pane_container); + rightPaneContainer.setPadding( + rightPaneContainer.getPaddingLeft(), + rightPaneContainer.getPaddingTop(), + rightPaneContainer.getPaddingRight(), + mBottomPadding); + requestLayout(); + } + + @Override + protected int getWidgetListHorizontalMargin() { + return getResources().getDimensionPixelSize( + R.dimen.widget_list_left_pane_horizontal_margin); + } + @Override protected boolean isTwoPane() { return true; diff --git a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java index cee7d6735a..b2620d0dd9 100644 --- a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java +++ b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java @@ -26,7 +26,7 @@ public interface SearchModeListener { /** * Notifies the subscriber when user enters widget picker search mode. */ - void enterSearchMode(); + void enterSearchMode(boolean shouldLog); /** * Notifies the subscriber when user exits widget picker search mode. diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java index a15508a617..2d96cbdc80 100644 --- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java +++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java @@ -70,7 +70,7 @@ public class WidgetsSearchBarController implements TextWatcher, mCancelButton.setVisibility(GONE); } else { mSearchAlgorithm.cancel(/* interruptActiveRequests= */ false); - mSearchModeListener.enterSearchMode(); + mSearchModeListener.enterSearchMode(true); mSearchAlgorithm.doSearch(mQuery, this); mCancelButton.setVisibility(VISIBLE); } diff --git a/src/com/android/launcher3/widget/util/WidgetSizes.java b/src/com/android/launcher3/widget/util/WidgetSizes.java index 7049509bd2..4688359a0c 100644 --- a/src/com/android/launcher3/widget/util/WidgetSizes.java +++ b/src/com/android/launcher3/widget/util/WidgetSizes.java @@ -15,8 +15,11 @@ */ package com.android.launcher3.widget.util; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.graphics.Point; @@ -91,20 +94,33 @@ public final class WidgetSizes { */ public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Context context, int spanX, int spanY) { - AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); - int widgetId = widgetView.getAppWidgetId(); - if (widgetId <= 0) { + updateWidgetSizeRangesAsync( + widgetView.getAppWidgetId(), widgetView.getAppWidgetInfo(), context, spanX, spanY); + } + + /** + * Updates a given {@code widgetId} with size, {@code spanX}, {@code spanY} asynchronously. + * + *

On Android S+, it also updates the given {@code widgetView} with a list of sizes derived + * from {@code spanX}, {@code spanY} in all supported device profiles. + */ + public static void updateWidgetSizeRangesAsync(int widgetId, + AppWidgetProviderInfo info, Context context, int spanX, int spanY) { + if (widgetId <= 0 || info == null) { return; } - Bundle sizeOptions = getWidgetSizeOptions(context, widgetView.getAppWidgetInfo().provider, - spanX, spanY); - if (sizeOptions.getParcelableArrayList( - AppWidgetManager.OPTION_APPWIDGET_SIZES).equals( - widgetManager.getAppWidgetOptions(widgetId).getParcelableArrayList( - AppWidgetManager.OPTION_APPWIDGET_SIZES))) { - return; - } - widgetManager.updateAppWidgetOptions(widgetId, sizeOptions); + + UI_HELPER_EXECUTOR.execute(() -> { + AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); + Bundle sizeOptions = getWidgetSizeOptions(context, info.provider, spanX, spanY); + if (sizeOptions.getParcelableArrayList( + AppWidgetManager.OPTION_APPWIDGET_SIZES).equals( + widgetManager.getAppWidgetOptions(widgetId).getParcelableArrayList( + AppWidgetManager.OPTION_APPWIDGET_SIZES))) { + return; + } + widgetManager.updateAppWidgetOptions(widgetId, sizeOptions); + }); } /** diff --git a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java index af4f22c287..6452ea2b64 100644 --- a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java +++ b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java @@ -39,13 +39,16 @@ public interface CustomWidgetPlugin extends Plugin { /** * Get the UUID for the custom widget. + * + * @deprecated Not used */ - String getId(); + @Deprecated + default String getId() { + return ""; + } /** * Used to modify a widgets' info. */ - default void updateWidgetInfo(AppWidgetProviderInfo info, Context context) { - - } + default void updateWidgetInfo(AppWidgetProviderInfo info, Context context) { } } diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java index 54cc0bc755..a940774ff0 100644 --- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java +++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java @@ -15,6 +15,8 @@ */ package com.android.systemui.plugins.shared; +import android.view.MotionEvent; + import java.io.PrintWriter; /** @@ -47,7 +49,11 @@ public interface LauncherOverlayManager { default void onActivityDestroyed() { } - interface LauncherOverlay { + /** + * @deprecated use LauncherOverlayTouchProxy directly + */ + @Deprecated + interface LauncherOverlay extends LauncherOverlayTouchProxy { /** * Touch interaction leading to overscroll has begun @@ -70,6 +76,38 @@ public interface LauncherOverlayManager { * @param callbacks A set of callbacks provided by Launcher in relation to the overlay */ void setOverlayCallbacks(LauncherOverlayCallbacks callbacks); + + @Override + default void onFlingVelocity(float velocity) { } + + @Override + default void onOverlayMotionEvent(MotionEvent ev, float scrollProgress) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN -> onScrollInteractionBegin(); + case MotionEvent.ACTION_MOVE -> onScrollChange(scrollProgress, false); + case MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> onScrollInteractionEnd(); + } + + } + } + + interface LauncherOverlayTouchProxy { + + /** + * Called just before finishing scroll interaction to indicate the fling velocity + */ + void onFlingVelocity(float velocity); + + /** + * Called to dispatch various motion events to the overlay + */ + void onOverlayMotionEvent(MotionEvent ev, float scrollProgress); + + /** + * Called when the launcher is ready to use the overlay + * @param callbacks A set of callbacks provided by Launcher in relation to the overlay + */ + default void setOverlayCallbacks(LauncherOverlayCallbacks callbacks) { } } interface LauncherOverlayCallbacks { diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java index 2f16065f37..8b983fc391 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java @@ -69,6 +69,34 @@ public class WidgetsModel { /* Map of widgets and shortcuts that are tracked per package. */ private final Map> mWidgetsList = new HashMap<>(); + /** + * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All + * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall + * list of {@link WidgetsListBaseEntry}s is not sorted. + * + * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) + */ + public synchronized ArrayList getFilteredWidgetsListForPicker( + Context context, + Predicate widgetItemFilter) { + ArrayList result = new ArrayList<>(); + AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context); + + for (Map.Entry> entry : mWidgetsList.entrySet()) { + PackageItemInfo pkgItem = entry.getKey(); + List widgetItems = entry.getValue() + .stream() + .filter(widgetItemFilter).toList(); + if (!widgetItems.isEmpty()) { + String sectionName = (pkgItem.title == null) ? "" : + indexer.computeSectionName(pkgItem.title); + result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems)); + result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems)); + } + } + return result; + } + /** * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row * are sorted (based on label and user), but the overall list of @@ -77,18 +105,8 @@ public class WidgetsModel { * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) */ public synchronized ArrayList getWidgetsListForPicker(Context context) { - ArrayList result = new ArrayList<>(); - AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context); - - for (Map.Entry> entry : mWidgetsList.entrySet()) { - PackageItemInfo pkgItem = entry.getKey(); - List widgetItems = entry.getValue(); - String sectionName = (pkgItem.title == null) ? "" : - indexer.computeSectionName(pkgItem.title); - result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems)); - result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems)); - } - return result; + // return all items + return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true); } /** Returns a mapping of packages to their widgets without static shortcuts. */ @@ -129,7 +147,8 @@ public class WidgetsModel { LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo); widgetsAndShortcuts.add(new WidgetItem( - launcherWidgetInfo, idp, app.getIconCache(), app.getContext())); + launcherWidgetInfo, idp, app.getIconCache(), app.getContext(), + widgetManager)); updatedItems.add(launcherWidgetInfo); } @@ -188,6 +207,7 @@ public class WidgetsModel { public void onPackageIconsUpdated(Set packageNames, UserHandle user, LauncherAppState app) { + WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext()); for (Entry> entry : mWidgetsList.entrySet()) { if (packageNames.contains(entry.getKey().packageName)) { List items = entry.getValue(); @@ -201,7 +221,7 @@ public class WidgetsModel { } else { items.set(i, new WidgetItem(item.widgetInfo, app.getInvariantDeviceProfile(), app.getIconCache(), - app.getContext())); + app.getContext(), widgetManager)); } } } @@ -319,4 +339,4 @@ public class WidgetsModel { return mMap.values(); } } -} \ No newline at end of file +} diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java index fe5c1fd000..efde7d863a 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java @@ -19,9 +19,11 @@ package com.android.launcher3.uioverrides; import android.app.ActivityOptions; import android.app.Person; import android.content.Context; +import android.content.Intent; import android.content.pm.LauncherActivityInfo; import android.content.pm.ShortcutInfo; import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; @@ -29,6 +31,7 @@ import android.util.ArrayMap; import com.android.launcher3.Utilities; import com.android.launcher3.util.UserIconInfo; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -80,6 +83,39 @@ public class ApiWrapper { return users; } + /** + * Returns the list of the system packages that are installed at user creation. + * An empty list denotes that all system packages are installed for that user at creation. + */ + public static List getPreInstalledSystemPackages(Context context, UserHandle user) { + return new ArrayList<>(); + } + + /** + * Returns an intent which can be used to start the App Market activity (Installer + * Activity). + */ + public static Intent getAppMarketActivityIntent(Context context, String packageName, + UserHandle user) { + return new Intent(Intent.ACTION_VIEW) + .setData(new Uri.Builder() + .scheme("market") + .authority("details") + .appendQueryParameter("id", packageName) + .build()) + .putExtra(Intent.EXTRA_REFERRER, new Uri.Builder().scheme("android-app") + .authority(context.getPackageName()).build()); + } + + /** + * Checks if an activity is flagged as non-resizeable. + */ + public static boolean isNonResizeableActivity(LauncherActivityInfo lai) { + // Overridden in quickstep + return false; + } + + private static class NoopDrawable extends ColorDrawable { @Override public int getIntrinsicHeight() { diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java index eb0494e2c2..b193d37895 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java @@ -18,6 +18,9 @@ package com.android.launcher3.uioverrides.flags; import static com.android.launcher3.config.FeatureFlags.FlagState.ENABLED; +import androidx.annotation.Nullable; + +import com.android.launcher3.ConstantItem; import com.android.launcher3.config.FeatureFlags.BooleanFlag; import com.android.launcher3.config.FeatureFlags.FlagState; import com.android.launcher3.config.FeatureFlags.IntFlag; @@ -54,6 +57,15 @@ public class FlagsFactory { return new IntFlag(defaultValueInCode); } + /** + * Creates a new debug integer flag and it is saved in LauncherPrefs. + */ + public static IntFlag getIntFlag( + int bugId, String key, int defaultValueInCode, String description, + @Nullable ConstantItem launcherPrefFlag) { + return new IntFlag(defaultValueInCode); + } + /** * Dumps the current flags state to the print writer */ diff --git a/tests/Android.bp b/tests/Android.bp index 84c3951199..e9111ea44a 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -21,35 +21,39 @@ package { filegroup { name: "launcher-tests-src", srcs: [ - "src/**/*.java", - "src/**/*.kt" + "src/**/*.java", + "src/**/*.kt", + "multivalentTests/src/**/*.java", + "multivalentTests/src/**/*.kt", ], exclude_srcs: [ - ":launcher-non-quickstep-tests-src" + ":launcher-non-quickstep-tests-src", ], } // Source code used for screenshot tests filegroup { - name: "launcher-image-tests-src", + name: "launcher-image-tests-helpers", srcs: [ - "src/com/android/launcher3/celllayout/board/*.java", - "src/com/android/launcher3/celllayout/board/*.kt", - "src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java", - "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", - "src/com/android/launcher3/ui/PortraitLandscapeRunner.java", - "src/com/android/launcher3/ui/TestViewHelpers.java", - "src/com/android/launcher3/util/LauncherLayoutBuilder.java", - "src/com/android/launcher3/util/ModelTestExtensions.kt", - "src/com/android/launcher3/util/TestConstants.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/*.java", - "src/com/android/launcher3/util/rule/*.kt", - "src/com/android/launcher3/util/viewcapture_analysis/*.java", - "src/com/android/launcher3/testcomponent/*.java", - "src/com/android/launcher3/testcomponent/*.kt", + "src/com/android/launcher3/celllayout/board/*.java", + "src/com/android/launcher3/celllayout/board/*.kt", + "src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java", + "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", + "src/com/android/launcher3/ui/PortraitLandscapeRunner.java", + "src/com/android/launcher3/ui/TestViewHelpers.java", + "multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java", + "src/com/android/launcher3/util/ModelTestExtensions.kt", + "src/com/android/launcher3/util/TestConstants.java", + "multivalentTests/src/com/android/launcher3/util/TestUtil.java", + "src/com/android/launcher3/util/Wait.java", + "multivalentTests/src/com/android/launcher3/util/WidgetUtils.java", + "src/com/android/launcher3/util/rule/*.java", + "src/com/android/launcher3/util/rule/*.kt", + "multivalentTests/src/com/android/launcher3/util/rule/*.java", + "multivalentTests/src/com/android/launcher3/util/rule/*.kt", + "src/com/android/launcher3/util/viewcapture_analysis/*.java", + "src/com/android/launcher3/testcomponent/*.java", + "src/com/android/launcher3/testcomponent/*.kt", ], } @@ -57,8 +61,8 @@ filegroup { filegroup { name: "launcher-non-quickstep-tests-src", srcs: [ - "src/com/android/launcher3/nonquickstep/**/*.java", - "src/com/android/launcher3/nonquickstep/**/*.kt", + "src/com/android/launcher3/nonquickstep/**/*.java", + "src/com/android/launcher3/nonquickstep/**/*.kt", ], } @@ -66,42 +70,42 @@ filegroup { filegroup { name: "launcher-oop-tests-src", srcs: [ - "src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java", - "src/com/android/launcher3/allapps/TaplTestsAllAppsIconsWorking.java", - "src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java", - "src/com/android/launcher3/dragging/TaplDragTest.java", - "src/com/android/launcher3/dragging/TaplUninstallRemove.java", - "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", - "src/com/android/launcher3/ui/PortraitLandscapeRunner.java", - "src/com/android/launcher3/ui/TaplTestsLauncher3.java", - "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java", - "src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java", - "src/com/android/launcher3/util/LauncherLayoutBuilder.java", - "src/com/android/launcher3/util/TestConstants.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/ViewCaptureRule.kt", - "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/TestIsolationRule.java", - "src/com/android/launcher3/util/rule/TestStabilityRule.java", - "src/com/android/launcher3/util/viewcapture_analysis/*.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", - "src/com/android/launcher3/testcomponent/ImeTestActivity.java", + "src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java", + "src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java", + "src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java", + "src/com/android/launcher3/dragging/TaplDragTest.java", + "src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java", + "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", + "src/com/android/launcher3/ui/PortraitLandscapeRunner.java", + "src/com/android/launcher3/ui/TaplTestsLauncher3Test.java", + "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java", + "src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java", + "multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java", + "src/com/android/launcher3/util/TestConstants.java", + "multivalentTests/src/com/android/launcher3/util/TestUtil.java", + "src/com/android/launcher3/util/Wait.java", + "multivalentTests/src/com/android/launcher3/util/WidgetUtils.java", + "src/com/android/launcher3/util/rule/FailureWatcher.java", + "src/com/android/launcher3/util/rule/ViewCaptureRule.kt", + "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/TestIsolationRule.java", + "multivalentTests/src/com/android/launcher3/util/rule/TestStabilityRule.java", + "src/com/android/launcher3/util/viewcapture_analysis/*.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", + "src/com/android/launcher3/testcomponent/ImeTestActivity.java", ], } // Library with all the dependencies for building quickstep android_library { name: "Launcher3TestLib", - srcs: [ ], + srcs: [], asset_dirs: ["assets"], resource_dirs: ["res"], static_libs: [ @@ -123,14 +127,19 @@ android_library { "testables", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", + "android.appwidget.flags-aconfig-java", ], manifest: "AndroidManifest-common.xml", platform_apis: true, + // TODO(b/319712088): re-enable use_resource_processor + use_resource_processor: false, } android_library { name: "Launcher3TestResources", resource_dirs: ["res"], + // TODO(b/319712088): re-enable use_resource_processor + use_resource_processor: false, } android_test { @@ -167,10 +176,89 @@ android_test { android_library { name: "launcher-testing-shared", srcs: [ - "shared/com/android/launcher3/testing/shared/**/*.java" + "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.java", + "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.kt" ], - resource_dirs: [ ], - manifest: "shared/AndroidManifest.xml", + resource_dirs: [], + manifest: "multivalentTests/shared/AndroidManifest.xml", sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, - } +} + +filegroup { + name: "launcher-testing-helpers", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + "multivalentTests/src/**/*.java", + "multivalentTests/src/**/*.kt", + "src/com/android/launcher3/ui/AbstractLauncherUiTest.java", + "tapl/com/android/launcher3/tapl/*.java", + "tapl/com/android/launcher3/tapl/*.kt", + ], + exclude_srcs: [ + // Test classes + "src/**/*Test.java", + "src/**/*Test.kt", + "multivalentTests/src/**/*Test.java", + "multivalentTests/src/**/*Test.kt", + ], +} + +android_library { + name: "Launcher3Lib", + srcs: [ + ":launcher-src", + ":launcher-src_shortcuts_overrides", + ":launcher-src_ui_overrides", + ], + static_libs: [ + "Launcher3CommonDepsLib", + ], + // TODO(b/319712088): re-enable use_resource_processor + use_resource_processor: false, +} + +android_robolectric_test { + enabled: true, + name: "Launcher3RoboTests", + // multivalentTests directory is a shared folder for not only robolectric converted test + // classes but also shared helper classes. + srcs: [ + "multivalentTests/src/com/android/launcher3/util/*.java", + "multivalentTests/src/com/android/launcher3/util/*.kt", + + // Test util classes + ":launcher-testing-helpers", + ":launcher-testing-shared", + ], + exclude_srcs: [ + //"src/com/android/launcher3/util/CellContentDimensionsTest.kt", // Failing - b/316553889 + + // requires modification to work with inline mock maker + "src/com/android/launcher3/util/rule/StaticMockitoRule.java", + ], + java_resource_dirs: ["config"], + static_libs: [ + "flag-junit-base", + "com_android_launcher3_flags_lib", + "com_android_wm_shell_flags_lib", + "androidx.test.uiautomator_uiautomator", + "androidx.core_core-animation-testing", + "androidx.test.ext.junit", + "inline-mockito-robolectric-prebuilt", + "platform-parametric-runner-lib", + "testables", + "Launcher3TestResources", + "SystemUISharedLib", + "launcher-testing-shared", + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "truth", + ], + instrumentation_for: "Launcher3", + upstream: true, +} diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index bd9da0ad4d..7cb7964757 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -88,6 +88,14 @@ android:resource="@xml/appwidget_dynamic_colors"/> + + + + + + @@ -149,7 +157,8 @@ android:name="com.android.launcher3.testcomponent.BaseTestingActivity" android:label="LauncherTestApp" android:exported="true" - android:taskAffinity="com.android.launcher3.testcomponent.Affinity1"> + android:taskAffinity="com.android.launcher3.testcomponent.Affinity1" + android:theme="@style/Theme.TestActivities"> @@ -292,6 +301,24 @@ + + + + + + + + + + + + + + + + + + + + + + + +

+ * If no task is focused, this will fail. + */ + public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_DOWN); + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP); + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT); + + mLauncher.executeAndWaitForLauncherStop( + () -> mLauncher.assertTrue( + "Failed to press enter", + mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)), + "pressing enter"); + mLauncher.assertAppLaunched(expectedPackageName); + + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "pressed enter")) { + return new LaunchedAppState(mLauncher); + } + } + } + private void verifyActionsViewVisibility() { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to assert overview actions view visibility")) { diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java index 1352cc07c1..b8adfe6656 100644 --- a/tests/tapl/com/android/launcher3/tapl/Folder.java +++ b/tests/tapl/com/android/launcher3/tapl/Folder.java @@ -16,6 +16,8 @@ package com.android.launcher3.tapl; +import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.test.uiautomator.UiObject2; @@ -58,4 +60,7 @@ public class Folder { return mLauncher.getWorkspace(); } } + Rect getDropLocationBounds() { + return mLauncher.getVisibleBounds(mContainer); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/FolderIcon.java b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java index 0c453bd5b5..080e52c337 100644 --- a/tests/tapl/com/android/launcher3/tapl/FolderIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java @@ -26,7 +26,7 @@ import com.android.launcher3.testing.shared.TestProtocol; /** * Folder Icon, an app folder in workspace. */ -public class FolderIcon implements FolderDragTarget { +public class FolderIcon implements IconDragTarget { protected final UiObject2 mObject; protected final LauncherInstrumentation mLauncher; @@ -60,7 +60,7 @@ public class FolderIcon implements FolderDragTarget { /** This method requires public access, however should not be called in tests. */ @Override - public FolderIcon getTargetFolder(Rect bounds) { + public FolderIcon getTargetIcon(Rect bounds) { return this; } } diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java index 9ca2dc8463..f8e1c108b6 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java @@ -30,8 +30,8 @@ public class HomeAllApps extends AllApps { /** * Swipes up or down to dismiss to Workspace. - * @param swipeDown Swipe all apps down to dismiss, otherwise swipe up to dismiss by going home. * + * @param swipeDown Swipe all apps down to dismiss, otherwise swipe up to dismiss by going home. * @return the Workspace object. */ @NonNull @@ -131,4 +131,20 @@ public class HomeAllApps extends AllApps { return new Workspace(mLauncher); } } + + @Override + protected void touchOutside(boolean tapRight, UiObject2 container) { + mLauncher.runToState( + () -> super.touchOutside(tapRight, container), + NORMAL_STATE_ORDINAL, + "touching outside"); + } + + @Override + protected void pressMetaKey() { + mLauncher.runToState( + () -> super.pressMetaKey(), + NORMAL_STATE_ORDINAL, + "pressing meta key"); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java b/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java index 693baa02c5..ca85b290c1 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeAppIcon.java @@ -27,7 +27,7 @@ import java.util.function.Supplier; /** * App icon on the workspace or all apps. */ -public abstract class HomeAppIcon extends AppIcon implements FolderDragTarget, WorkspaceDragSource { +public abstract class HomeAppIcon extends AppIcon implements IconDragTarget, WorkspaceDragSource { private final String mAppName; @@ -42,7 +42,7 @@ public abstract class HomeAppIcon extends AppIcon implements FolderDragTarget, W * @param target the destination icon. */ @NonNull - public FolderIcon dragToIcon(FolderDragTarget target) { + public FolderIcon dragToIcon(IconDragTarget target) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to drag icon")) { final Rect dropBounds = target.getDropLocationBounds(); @@ -51,13 +51,33 @@ public abstract class HomeAppIcon extends AppIcon implements FolderDragTarget, W () -> { final Rect bounds = target.getDropLocationBounds(); return new Point(bounds.centerX(), bounds.centerY()); - }); - FolderIcon result = target.getTargetFolder(dropBounds); + }, false); + FolderIcon result = target.getTargetIcon(dropBounds); mLauncher.assertTrue("Can't find the target folder.", result != null); return result; } } + /** + * Drag the AppIcon to the given position of a folder icon, and then inside that folder. + * + * @param target the destination folder. + */ + @NonNull + public Folder dragToFolder(IconDragTarget target) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to drag icon")) { + Workspace.dragIconToWorkspace( + mLauncher, this, + () -> { + final Rect bounds = target.getDropLocationBounds(); + return new Point(bounds.centerX(), bounds.centerY()); + }, /* isDraggingToFolder */ true); + } + return new Folder(mLauncher); + } + + /** This method requires public access, however should not be called in tests. */ @Override public Rect getDropLocationBounds() { @@ -66,7 +86,7 @@ public abstract class HomeAppIcon extends AppIcon implements FolderDragTarget, W /** This method requires public access, however should not be called in tests. */ @Override - public FolderIcon getTargetFolder(Rect bounds) { + public FolderIcon getTargetIcon(Rect bounds) { for (FolderIcon folderIcon : mLauncher.getWorkspace().getFolderIcons()) { final Rect folderIconBounds = folderIcon.getDropLocationBounds(); if (bounds.contains(folderIconBounds.centerX(), folderIconBounds.centerY())) { diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java index 5385c65165..c1fc45f2f6 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.tapl; +import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL; + import androidx.test.uiautomator.UiObject2; /** @@ -25,4 +27,13 @@ class HomeQsb extends Qsb { HomeQsb(LauncherInstrumentation launcher, UiObject2 hotseat) { super(launcher, hotseat, "search_container_hotseat"); } + + @Override + protected void clickQsb() { + // Clicking Qsb will switch to All Apps state. + mLauncher.runToState( + () -> super.clickQsb(), + ALL_APPS_STATE_ORDINAL, + "Clicking Qsb"); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java b/tests/tapl/com/android/launcher3/tapl/IconDragTarget.java similarity index 91% rename from tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java rename to tests/tapl/com/android/launcher3/tapl/IconDragTarget.java index 2c60668ba8..2f86703d9b 100644 --- a/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java +++ b/tests/tapl/com/android/launcher3/tapl/IconDragTarget.java @@ -18,11 +18,11 @@ package com.android.launcher3.tapl; import android.graphics.Rect; -public interface FolderDragTarget { +public interface IconDragTarget { /** 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); + FolderIcon getTargetIcon(Rect bounds); } diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java index a1d8059631..5ef82ca897 100644 --- a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java +++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java @@ -58,15 +58,15 @@ public final class KeyboardQuickSwitch { private final LauncherInstrumentation mLauncher; private final LauncherInstrumentation.ContainerType mStartingContainerType; - private final boolean mExpectHomeKeyEventsOnDismiss; + private final boolean mIsHomeState; KeyboardQuickSwitch( LauncherInstrumentation launcher, LauncherInstrumentation.ContainerType startingContainerType, - boolean expectHomeKeyEventsOnDismiss) { + boolean isHomeState) { mLauncher = launcher; mStartingContainerType = startingContainerType; - mExpectHomeKeyEventsOnDismiss = expectHomeKeyEventsOnDismiss; + mIsHomeState = isHomeState; } /** @@ -164,7 +164,7 @@ public final class KeyboardQuickSwitch { mLauncher.verifyContainerType(mStartingContainerType); // Wait until the device has fully settled before unpressing the key code - if (mExpectHomeKeyEventsOnDismiss) { + if (mIsHomeState) { mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_HOME_ALT_LEFT_UP); } mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0); @@ -204,7 +204,14 @@ public final class KeyboardQuickSwitch { "want to launch focused task: " + (expectedPackageName == null ? "Overview" : expectedPackageName))) { mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KQS_ALT_LEFT_UP); - mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0); + + if (expectedPackageName == null || !mIsHomeState) { + mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0); + } else { + mLauncher.executeAndWaitForLauncherStop( + () -> mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0), + "unpressing left alt"); + } try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer( "un-pressed left alt")) { diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java index fe927b3fd0..9d3bc6e952 100644 --- a/tests/tapl/com/android/launcher3/tapl/Launchable.java +++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java @@ -16,11 +16,11 @@ package com.android.launcher3.tapl; -import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; - import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE; import android.graphics.Point; +import android.util.Log; import android.view.MotionEvent; import androidx.test.uiautomator.UiObject2; @@ -58,8 +58,8 @@ public abstract class Launchable { */ public LaunchedAppState launch(String expectedPackageName) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( - "want to launch an app from " + launchableType())) { + try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(String.format( + "want to launch an app (%s) from %s", expectedPackageName, launchableType()))) { LauncherInstrumentation.log("Launchable.launch before click " + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject)); @@ -97,12 +97,10 @@ public abstract class Launchable { LauncherInstrumentation.log("Launchable.launch before click " + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds( mObject)); - mLauncher.executeAndWaitForLauncherEvent( + + mLauncher.executeAndWaitForLauncherStop( () -> mLauncher.clickLauncherObject(mObject), - accessibilityEvent -> - accessibilityEvent.getEventType() == TYPE_WINDOW_STATE_CHANGED, - () -> "Unable to click object to launch split", - "Click launcher object to launch split"); + "clicking the launchable"); try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) { mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, OverviewTask.SPLIT_START_EVENT); @@ -117,11 +115,15 @@ public abstract class Launchable { iconCenter.y - getStartDragThreshold()); if (runToSpringLoadedState) { + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "Launchable.startDrag: actionName: long-pressing and triggering drag start" + + " iconCenter: " + iconCenter + " dragStartCenter: " + + dragStartCenter); mLauncher.runToState(() -> movePointerForStartDrag( - downTime, - iconCenter, - dragStartCenter, - expectLongClickEvents), + downTime, + iconCenter, + dragStartCenter, + expectLongClickEvents), SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start"); } else { movePointerForStartDrag( diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java index 184ece74f5..b5414b7cb8 100644 --- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java +++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java @@ -24,7 +24,9 @@ import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_B import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_SHELL_DRAG_READY; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_SCALE; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_TASKBAR_FROM_NAV_THRESHOLD; +import static com.android.launcher3.testing.shared.TestProtocol.SUCCESSFUL_GESTURE_MISMATCH_EVENTS; import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD; +import static com.android.launcher3.testing.shared.TestProtocol.testLogD; import android.graphics.Point; import android.graphics.Rect; @@ -130,12 +132,17 @@ public final class LaunchedAppState extends Background { int endX = startX; int endY = startY - taskbarFromNavThreshold; - mLauncher.linearGesture(startX, startY, endX, endY, 10, /* slowDown= */ true, - LauncherInstrumentation.GestureScope.EXPECT_PILFER); + mLauncher.executeAndWaitForLauncherStop( + () -> mLauncher.linearGesture(startX, startY, endX, endY, 10, + /* slowDown= */ true, + LauncherInstrumentation.GestureScope.EXPECT_PILFER), + "swiping"); LauncherInstrumentation.log("swipeUpToUnstashTaskbar: sent linear swipe up gesture"); return new Taskbar(mLauncher); } finally { + testLogD(SUCCESSFUL_GESTURE_MISMATCH_EVENTS, + "swipeUpToUnstashTaskbar: completed gesture"); mLauncher.getTestInfo(REQUEST_DISABLE_BLOCK_TIMEOUT); } } @@ -346,10 +353,14 @@ public final class LaunchedAppState extends Background { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to press back from launched app to workspace")) { - mLauncher.executeAndWaitForWallpaperAnimation( - () -> mLauncher.pressBackImpl(), - "pressing back" - ); + if (mLauncher.isLauncher3()) { + mLauncher.pressBackImpl(); + } else { + mLauncher.executeAndWaitForWallpaperAnimation( + () -> mLauncher.pressBackImpl(), + "pressing back" + ); + } return new Workspace(mLauncher); } } diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 91ef472ee4..053b360645 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -21,13 +21,19 @@ import static android.content.pm.PackageManager.DONT_KILL_APP; import static android.content.pm.PackageManager.MATCH_ALL; import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; import static android.view.KeyEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_SCROLL; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT; import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID; import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_NUM_ALL_APPS_COLUMNS; +import static com.android.launcher3.testing.shared.TestProtocol.SUCCESSFUL_GESTURE_MISMATCH_EVENTS; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD; +import static com.android.launcher3.testing.shared.TestProtocol.testLogD; import android.app.ActivityManager; import android.app.Instrumentation; @@ -91,7 +97,6 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.TimeoutException; import java.util.function.BooleanSupplier; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -197,10 +202,11 @@ public final class LauncherInstrumentation { private boolean mIgnoreTaskbarVisibility = false; - private Consumer mOnSettledStateAction; - private LogEventChecker mEventChecker; + // UI anomaly checker provided by the test. + private Runnable mTestAnomalyChecker; + private boolean mCheckEventsForSuccessfulGestures = false; private Runnable mOnLauncherCrashed; @@ -277,21 +283,35 @@ public final class LauncherInstrumentation { assertNotNull("Cannot find content provider for " + testProviderAuthority, pi); ComponentName cn = new ComponentName(pi.packageName, pi.name); + final int iterations = isLauncherTest ? 300 : 100; + if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) { if (TestHelpers.isInLauncherProcess()) { pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP); } else { try { final int userId = getContext().getUserId(); + final String launcherPidCommand = "pidof " + pi.packageName; + final String initialPid = mDevice.executeShellCommand(launcherPidCommand); + mDevice.executeShellCommand( "pm enable --user " + userId + " " + cn.flattenToString()); + + // Wait for Launcher restart after enabling test provider. + for (int i = 0; i < iterations; ++i) { + final String currentPid = mDevice.executeShellCommand(launcherPidCommand) + .replaceAll("\\s", ""); + if (!currentPid.isEmpty() && !currentPid.equals(initialPid)) break; + if (i == iterations - 1) { + fail("Launcher didn't restart after enabling test provider"); + } + SystemClock.sleep(100); + } } catch (IOException e) { fail(e.toString()); } } - final int iterations = isLauncherTest ? 300 : 100; - // Wait for Launcher content provider to become enabled. for (int i = 0; i < iterations; ++i) { final ContentProviderClient testProvider = getContext().getContentResolver() @@ -370,8 +390,8 @@ public final class LauncherInstrumentation { .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD); } - Insets getImeInsets() { - return getTestInfo(TestProtocol.REQUEST_IME_INSETS) + Insets getSystemGestureRegion() { + return getTestInfo(TestProtocol.REQUEST_SYSTEM_GESTURE_REGION) .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD); } @@ -385,11 +405,21 @@ public final class LauncherInstrumentation { .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + public boolean isTaskbarNavbarUnificationEnabled() { + return getTestInfo(TestProtocol.REQUEST_ENABLE_TASKBAR_NAVBAR_UNIFICATION) + .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + public boolean isTwoPanels() { return getTestInfo(TestProtocol.REQUEST_IS_TWO_PANELS) .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + int getCellLayoutBoarderHeight() { + return getTestInfo(TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + int getFocusedTaskHeightForTablet() { return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt( TestProtocol.TEST_INFO_RESPONSE_FIELD); @@ -405,6 +435,11 @@ public final class LauncherInstrumentation { .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + public int getOverviewCurrentPageIndex() { + return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX) + .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + float getExactScreenCenterX() { return getRealDisplaySize().x / 2f; } @@ -546,8 +581,31 @@ public final class LauncherInstrumentation { checkForAnomaly(false, false); } + /** + * Allows the test to provide a pluggable anomaly checker. It’s supposed to throw an exception + * if the check fails. The test may provide its own anomaly checker, for example, if it wants to + * check for an anomaly that’s recognized by the standard TAPL anomaly checker, but wants a + * custom error message, such as adding information whether the keyguard is seen for the first + * time during the shard execution. + */ + public void setAnomalyChecker(Runnable anomalyChecker) { + mTestAnomalyChecker = anomalyChecker; + } + + /** + * Verifies that there are no visible UI anomalies. An "anomaly" is a state of UI that should + * never happen during the text execution. Anomaly is something different from just “regular” + * unexpected state of the Launcher such as when we see Workspace after swiping up to All Apps. + * Workspace is a normal state. We can contrast this with an anomaly, when, for example, we see + * a lock screen. Launcher tests can never bring the lock screen, so the very presence of the + * lock screen is an indication that something went very wrong, and perhaps is caused by reasons + * outside of the Launcher and its tests, perhaps, by a crash in System UI. Diagnosing anomalies + * helps to understand faster whether the problem is in the Launcher or its tests, or outside. + */ public void checkForAnomaly( boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) { + if (mTestAnomalyChecker != null) mTestAnomalyChecker.run(); + final String systemAnomalyMessage = getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews); if (systemAnomalyMessage != null) { @@ -597,10 +655,6 @@ public final class LauncherInstrumentation { this.mSystemHealthSupplier = supplier; } - public void setOnSettledStateAction(Consumer onSettledStateAction) { - mOnSettledStateAction = onSettledStateAction; - } - public void onTestStart() { mTestStartTime = System.currentTimeMillis(); } @@ -792,7 +846,8 @@ public final class LauncherInstrumentation { } private String getNavigationButtonResPackage() { - return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE; + return isTablet() || isTaskbarNavbarUnificationEnabled() + ? getLauncherPackageName() : SYSTEMUI_PACKAGE; } UiObject2 verifyContainerType(ContainerType containerType) { @@ -810,8 +865,6 @@ public final class LauncherInstrumentation { final UiObject2 container = verifyVisibleObjects(containerType); - if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType); - return container; } @@ -853,7 +906,6 @@ public final class LauncherInstrumentation { waitUntilLauncherObjectGone(WORKSPACE_RES_ID); waitUntilLauncherObjectGone(WIDGETS_RES_ID); waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID); - waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID); waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID); if (is3PLauncher() && isTablet() && !isTransientTaskbar()) { @@ -862,6 +914,12 @@ public final class LauncherInstrumentation { waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID); } + boolean splitSelectionActive = getTestInfo(REQUEST_GET_SPLIT_SELECTION_ACTIVE) + .getBoolean(TEST_INFO_RESPONSE_FIELD); + if (!splitSelectionActive) { + waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID); + } // do nothing, we expect that view + return waitForLauncherObject(APPS_RES_ID); } case OVERVIEW: @@ -1007,11 +1065,11 @@ public final class LauncherInstrumentation { return; } - linearGesture( - displaySize.x / 2, displaySize.y - 1, - displaySize.x / 2, 0, - ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, - false, GestureScope.EXPECT_PILFER); + if (isLauncher3()) { + gestureToDismissPopup(displaySize); + } else { + runToState(() -> gestureToDismissPopup(displaySize), NORMAL_STATE_ORDINAL, "swiping"); + } try (LauncherInstrumentation.Closable c1 = addContextLayer( String.format("Swiped up from floating view %s to home", floatingRes.get()))) { @@ -1020,6 +1078,14 @@ public final class LauncherInstrumentation { } } + private void gestureToDismissPopup(Point displaySize) { + linearGesture( + displaySize.x / 2, displaySize.y - 1, + displaySize.x / 2, 0, + ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, + false, GestureScope.EXPECT_PILFER); + } + /** * @return the Workspace object. * @deprecated use goHome(). @@ -1100,7 +1166,11 @@ public final class LauncherInstrumentation { log("Hierarchy before clicking home:"); dumpViewHierarchy(); action = "clicking home button"; - + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.goHome: isThreeFingerTrackpadGesture: " + + isThreeFingerTrackpadGesture + + "getNavigationModel() == NavigationModel.ZERO_BUTTON: " + ( + getNavigationModel() == NavigationModel.ZERO_BUTTON)); runToState( getHomeButton()::click, NORMAL_STATE_ORDINAL, @@ -1144,7 +1214,8 @@ public final class LauncherInstrumentation { waitForNavigationUiObject("back").click(); } if (launcherVisible) { - if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) { + if (InstrumentationRegistry.getTargetContext().getApplicationInfo() + .isOnBackInvokedCallbackEnabled()) { expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED); } else { expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN); @@ -1446,6 +1517,8 @@ public final class LauncherInstrumentation { @NonNull UiObject2 waitForLauncherObject(String resName) { + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.waitForLauncherObject"); return waitForObjectBySelector(getLauncherObjectSelector(resName)); } @@ -1475,12 +1548,16 @@ public final class LauncherInstrumentation { @NonNull List waitForObjectsBySelector(BySelector selector) { + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.waitForObjectsBySelector"); final List objects = mDevice.wait(Until.findObjects(selector), WAIT_TIME_MS); assertNotNull("Can't find any view in Launcher, selector: " + selector, objects); return objects; } private UiObject2 waitForObjectBySelector(BySelector selector) { + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.waitForObjectBySelector"); final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS); assertNotNull("Can't find a view in Launcher, selector: " + selector, object); return object; @@ -1523,13 +1600,17 @@ public final class LauncherInstrumentation { void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) { if (requireEvent) { + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.runToState: command: " + command + " expectedState: " + + expectedState + " actionName: " + actionName + "requireEvent: true"); runToState(command, expectedState, actionName); } else { command.run(); } } - void runToState(Runnable command, int expectedState, String actionName) { + /** Run an action and wait for the specified Launcher state. */ + public void runToState(Runnable command, int expectedState, String actionName) { final List actualEvents = new ArrayList<>(); executeAndWaitForLauncherEvent( command, @@ -1702,6 +1783,16 @@ public final class LauncherInstrumentation { "scrolling"); } + void pointerScroll(float pointerX, float pointerY, Direction direction) { + executeAndWaitForLauncherEvent( + () -> injectEvent(getPointerMotionEvent( + ACTION_SCROLL, pointerX, pointerY, direction)), + event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()), + () -> "Didn't receive a scroll end message: " + direction + " scroll from (" + + pointerX + ", " + pointerY + ")", + "scrolling"); + } + // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a // fixed interval each time. public void linearGesture(int startX, int startY, int endX, int endY, int steps, @@ -1710,32 +1801,38 @@ public final class LauncherInstrumentation { final long downTime = SystemClock.uptimeMillis(); final Point start = new Point(startX, startY); final Point end = new Point(endX, endY); + long endTime = downTime; sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope); - if (mTrackpadGestureType != TrackpadGestureType.NONE) { - sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1), - start, gestureScope); - if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER - || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) { + try { + + if (mTrackpadGestureType != TrackpadGestureType.NONE) { sendPointer(downTime, downTime, - getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2), + getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1), start, gestureScope); - if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) { + if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER + || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) { sendPointer(downTime, downTime, - getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3), + getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2), + start, gestureScope); + if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) { + sendPointer(downTime, downTime, + getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3), + start, gestureScope); + } + } + } + endTime = movePointer( + start, end, steps, false, downTime, downTime, slowDown, gestureScope); + } finally { + if (mTrackpadGestureType != TrackpadGestureType.NONE) { + for (int i = mPointerCount; i >= 2; i--) { + sendPointer(downTime, downTime, + getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1), start, gestureScope); } } + sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope); } - final long endTime = movePointer( - start, end, steps, false, downTime, downTime, slowDown, gestureScope); - if (mTrackpadGestureType != TrackpadGestureType.NONE) { - for (int i = mPointerCount; i >= 2; i--) { - sendPointer(downTime, downTime, - getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1), - start, gestureScope); - } - } - sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope); } private static int getPointerAction(int action, int index) { @@ -1765,6 +1862,41 @@ public final class LauncherInstrumentation { return getContext().getResources(); } + private static MotionEvent getPointerMotionEvent( + int action, float x, float y, Direction direction) { + MotionEvent.PointerCoords[] coordinates = new MotionEvent.PointerCoords[1]; + coordinates[0] = new MotionEvent.PointerCoords(); + coordinates[0].x = x; + coordinates[0].y = y; + boolean isVertical = direction == Direction.UP || direction == Direction.DOWN; + boolean isForward = direction == Direction.RIGHT || direction == Direction.DOWN; + coordinates[0].setAxisValue( + isVertical ? MotionEvent.AXIS_VSCROLL : MotionEvent.AXIS_HSCROLL, + isForward ? 1f : -1f); + + MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1]; + properties[0] = new MotionEvent.PointerProperties(); + properties[0].id = 0; + properties[0].toolType = MotionEvent.TOOL_TYPE_MOUSE; + + final long downTime = SystemClock.uptimeMillis(); + return MotionEvent.obtain( + downTime, + downTime, + action, + /* pointerCount= */ 1, + properties, + coordinates, + /* metaState= */ 0, + /* buttonState= */ 0, + /* xPrecision= */ 1f, + /* yPrecision= */ 1f, + /* deviceId= */ 0, + /* edgeFlags= */ 0, + InputDevice.SOURCE_CLASS_POINTER, + /* flags= */ 0); + } + private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime, int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) { MotionEvent.PointerProperties[] pointerProperties = @@ -1848,11 +1980,15 @@ public final class LauncherInstrumentation { mPointerCount = 1; pointerCount = mPointerCount; } + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.sendPointer: ACTION_DOWN"); break; case MotionEvent.ACTION_UP: if (hasTIS && gestureScope == GestureScope.EXPECT_PILFER) { expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS); } + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "LauncherInstrumentation.sendPointer: ACTION_UP"); break; case MotionEvent.ACTION_POINTER_DOWN: mPointerCount++; @@ -1975,11 +2111,14 @@ public final class LauncherInstrumentation { final long downTime = SystemClock.uptimeMillis(); sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.DONT_EXPECT_PILFER); - expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent); - final UiObject2 result = waitForLauncherObject(resName); - sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter, - GestureScope.DONT_EXPECT_PILFER); - return result; + try { + expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent); + final UiObject2 result = waitForLauncherObject(resName); + return result; + } finally { + sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter, + GestureScope.DONT_EXPECT_PILFER); + } } @NonNull @@ -1990,12 +2129,15 @@ public final class LauncherInstrumentation { sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE, /* isRightClick= */ true); - expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent); - final UiObject2 result = waitForLauncherObject(resName); - sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter, - GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE, - /* isRightClick= */ true); - return result; + try { + expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent); + final UiObject2 result = waitForLauncherObject(resName); + return result; + } finally { + sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter, + GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE, + /* isRightClick= */ true); + } } private static int getSystemIntegerRes(Context context, String resName) { @@ -2094,6 +2236,11 @@ public final class LauncherInstrumentation { getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED); } + /** Shows the bubble bar if it is stashed, otherwise this does nothing. */ + public void showBubbleBarIfHidden() { + getTestInfo(TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED); + } + /** Blocks the taskbar from automatically stashing based on time. */ public void enableBlockTimeout(boolean enable) { getTestInfo(enable @@ -2106,6 +2253,11 @@ public final class LauncherInstrumentation { .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + public boolean isImeDocked() { + return getTestInfo(TestProtocol.REQUEST_TASKBAR_IME_DOCKED).getBoolean( + TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + /** Enables transient taskbar for testing purposes only. */ public void enableTransientTaskbar(boolean enable) { getTestInfo(enable @@ -2177,9 +2329,13 @@ public final class LauncherInstrumentation { } if (mEventChecker != null) { + testLogD(SUCCESSFUL_GESTURE_MISMATCH_EVENTS, "eventsCheck: mEventChecker exists"); mEventChecker = null; if (mCheckEventsForSuccessfulGestures) { final String message = eventChecker.verify(WAIT_TIME_MS, true); + testLogD(SUCCESSFUL_GESTURE_MISMATCH_EVENTS, + "mCheckEventsForSuccessfulGestures = true | eventsCheck: message=" + + message); if (message != null) { dumpDiagnostics(message); checkForAnomaly(); @@ -2287,12 +2443,13 @@ public final class LauncherInstrumentation { : containerBounds.left - 1; } // If IME is visible and overlaps the container bounds, touch above it. + final Insets systemGestureRegion = getSystemGestureRegion(); int bottomBound = Math.min( containerBounds.bottom, - getRealDisplaySize().y - getImeInsets().bottom); - int y = (bottomBound + containerBounds.top) / 2; + getRealDisplaySize().y - systemGestureRegion.bottom); + int y = (bottomBound - containerBounds.top) / 2; // Do not tap in the status bar. - y = Math.max(y, getWindowInsets().top); + y = Math.max(y, systemGestureRegion.top); final long downTime = SystemClock.uptimeMillis(); final Point tapTarget = new Point(x, y); diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java index 672c6e0c8c..70d63bd817 100644 --- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java +++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java @@ -18,6 +18,8 @@ package com.android.launcher3.tapl; import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_MAIN; import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_PILFER; import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_TIS; +import static com.android.launcher3.testing.shared.TestProtocol.SUCCESSFUL_GESTURE_MISMATCH_EVENTS; +import static com.android.launcher3.testing.shared.TestProtocol.testLogD; import android.os.SystemClock; @@ -88,6 +90,7 @@ public class LogEventChecker { } String verify(long waitForExpectedCountMs, boolean successfulGesture) { + testLogD(SUCCESSFUL_GESTURE_MISMATCH_EVENTS, "LogEventChecker.java - verify"); final ListMap actualEvents = finishSync(waitForExpectedCountMs); if (actualEvents == null) return "null event sequences because launcher likely died"; diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java index bd2c9c1bd4..d7c40a0859 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java @@ -89,8 +89,7 @@ public class OverviewActions { private SelectModeButtons getSelectModeButtons() { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to get select mode buttons")) { - UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons"); - return new SelectModeButtons(selectModeButtons, mLauncher); + return new SelectModeButtons(mLauncher); } } diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index f383e99584..afe57223f9 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -16,6 +16,10 @@ package com.android.launcher3.tapl; +import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.DEFAULT; +import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT; +import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT; + import android.graphics.Rect; import androidx.annotation.NonNull; @@ -34,9 +38,6 @@ import java.util.stream.Collectors; */ public final class OverviewTask { private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; - private static final String TASK_SNAPSHOT_1 = "snapshot"; - private static final String TASK_SNAPSHOT_2 = "bottomright_snapshot"; - static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync"); static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect"); static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks"); @@ -64,15 +65,16 @@ public final class OverviewTask { return getCombinedSplitTaskHeight(); } - return mTask.getVisibleBounds().height(); + UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes); + return taskSnapshot1.getVisibleBounds().height(); } /** * Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider. */ private int getCombinedSplitTaskHeight() { - UiObject2 taskSnapshot1 = findObjectInTask(TASK_SNAPSHOT_1); - UiObject2 taskSnapshot2 = findObjectInTask(TASK_SNAPSHOT_2); + UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); + UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); // If the split task is partly off screen, taskSnapshot1 can be invisible. if (taskSnapshot1 == null) { @@ -96,15 +98,16 @@ public final class OverviewTask { return getCombinedSplitTaskWidth(); } - return mTask.getVisibleBounds().width(); + UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes); + return taskSnapshot1.getVisibleBounds().width(); } /** * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider. */ private int getCombinedSplitTaskWidth() { - UiObject2 taskSnapshot1 = findObjectInTask(TASK_SNAPSHOT_1); - UiObject2 taskSnapshot2 = findObjectInTask(TASK_SNAPSHOT_2); + UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); + UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); int left = Math.min( taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left); @@ -115,15 +118,15 @@ public final class OverviewTask { } int getTaskCenterX() { - return mTask.getParent().getVisibleCenter().x; + return mTask.getVisibleCenter().x; } int getTaskCenterY() { - return mTask.getParent().getVisibleCenter().y; + return mTask.getVisibleCenter().y; } float getExactCenterX() { - return mTask.getParent().getVisibleBounds().exactCenterX(); + return mTask.getVisibleBounds().exactCenterX(); } UiObject2 getUiObject() { @@ -225,11 +228,17 @@ public final class OverviewTask { /** Taps the task menu. Returns the task menu object. */ @NonNull public OverviewTaskMenu tapMenu() { + return tapMenu(DEFAULT); + } + + /** Taps the task menu of the split task. Returns the split task's menu object. */ + @NonNull + public OverviewTaskMenu tapMenu(OverviewSplitTask task) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to tap the task menu")) { mLauncher.clickLauncherObject( - mLauncher.waitForObjectInContainer(mTask.getParent(), "icon")); + mLauncher.waitForObjectInContainer(mTask, task.iconAppRes)); try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "tapped the task menu")) { @@ -238,27 +247,31 @@ public final class OverviewTask { } } - /** Taps the task menu of the split task. Returns the split task's menu object. */ - @NonNull - public OverviewTaskMenu tapSplitTaskMenu() { - try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); - LauncherInstrumentation.Closable c = mLauncher.addContextLayer( - "want to tap the split task's menu")) { - mLauncher.clickLauncherObject( - mLauncher.waitForObjectInContainer(mTask.getParent(), "bottomRight_icon")); - - try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( - "tapped the split task's menu")) { - return new OverviewTaskMenu(mLauncher); - } - } - } - boolean isTaskSplit() { - return findObjectInTask(TASK_SNAPSHOT_2) != null; + return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null; } private UiObject2 findObjectInTask(String resName) { - return mTask.getParent().findObject(mLauncher.getOverviewObjectSelector(resName)); + return mTask.findObject(mLauncher.getOverviewObjectSelector(resName)); + } + + /** + * Enum used to specify which task is retrieved when it is a split task. + */ + public enum OverviewSplitTask { + // The main task when the task is not split. + DEFAULT("snapshot", "icon"), + // The first task in split task. + SPLIT_TOP_OR_LEFT("snapshot", "icon"), + // The second task in split task. + SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"); + + public final String snapshotRes; + public final String iconAppRes; + + OverviewSplitTask(String snapshotRes, String iconAppRes) { + this.snapshotRes = snapshotRes; + this.iconAppRes = iconAppRes; + } } } diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java index 3d2914d754..902ad5b568 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java @@ -16,6 +16,9 @@ package com.android.launcher3.tapl; +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL; + import androidx.annotation.NonNull; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiObject2; @@ -40,8 +43,11 @@ public class OverviewTaskMenu { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "tap split menu item")) { - mLauncher.clickLauncherObject( - mLauncher.findObjectInContainer(mMenu, By.textStartsWith("Split"))); + mLauncher.runToState(() -> mLauncher.clickLauncherObject( + mLauncher.findObjectInContainer(mMenu, By.textStartsWith("Split"))), + OVERVIEW_SPLIT_SELECT_ORDINAL, + "tapping split menu item" + ); try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "tapped split menu item")) { @@ -72,6 +78,25 @@ public class OverviewTaskMenu { } } + /** Taps the select menu item from the overview task menu. */ + @NonNull + public SelectModeButtons tapSelectMenuItem() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "before tapping the select menu item")) { + + mLauncher.runToState( + () -> mLauncher.clickLauncherObject( + mLauncher.findObjectInContainer(mMenu, By.text("Select"))), + OVERVIEW_MODAL_TASK_STATE_ORDINAL, "tapping select menu item"); + + try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( + "select menu item opened")) { + return new SelectModeButtons(mLauncher); + } + } + } + /** Returns true if an item matching the given string is present in the menu. */ public boolean hasMenuItem(String expectedMenuItemText) { UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText)); diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java index fe2a63dd57..d67b8a3616 100644 --- a/tests/tapl/com/android/launcher3/tapl/Qsb.java +++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java @@ -118,9 +118,7 @@ public abstract class Qsb implements SearchInputSource { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "want to open search result page"); LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - mLauncher.clickLauncherObject(waitForQsbObject()); - // wait for the result rendering to complete - mLauncher.waitForIdle(); + clickQsb(); try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer( "clicked qsb to open search result page")) { return createSearchResult(); @@ -128,6 +126,10 @@ public abstract class Qsb implements SearchInputSource { } } + protected void clickQsb() { + mLauncher.clickLauncherObject(waitForQsbObject()); + } + @Override public LauncherInstrumentation getLauncher() { return mLauncher; diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java index f0a8aa26c0..8d3a631744 100644 --- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.tapl; +import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; + import android.widget.TextView; import androidx.test.uiautomator.By; @@ -39,7 +41,7 @@ public class SearchResultFromQsb implements SearchInputSource { /** Find the app from search results with app name. */ public AppIcon findAppIcon(String appName) { - UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName)); + UiObject2 icon = mLauncher.waitForLauncherObject(AppIcon.getAppIconSelector(appName)); return createAppIcon(icon); } @@ -87,7 +89,7 @@ public class SearchResultFromQsb implements SearchInputSource { + (tapRight ? "right" : "left"))) { final UiObject2 allAppsBottomSheet = mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID); - mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight); + tapOutside(tapRight, allAppsBottomSheet); try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer( "tapped outside AllApps bottom sheet")) { verifyVisibleContainerOnDismiss(); @@ -95,6 +97,13 @@ public class SearchResultFromQsb implements SearchInputSource { } } + protected void tapOutside(boolean tapRight, UiObject2 allAppsBottomSheet) { + mLauncher.runToState( + () -> mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight), + NORMAL_STATE_ORDINAL, + "tappig outside"); + } + protected void verifyVisibleContainerOnDismiss() { mLauncher.getWorkspace(); } diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java index 00291a3381..f4b4a91744 100644 --- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java @@ -50,4 +50,9 @@ public class SearchResultFromTaskbarQsb extends SearchResultFromQsb { protected void verifyVisibleContainerOnDismiss() { mLauncher.getLaunchedAppState().assertTaskbarVisible(); } + + @Override + protected void tapOutside(boolean tapRight, UiObject2 allAppsBottomSheet) { + mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java index e1b73a4aa3..e2bc17bfb4 100644 --- a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java +++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java @@ -16,9 +16,17 @@ package com.android.launcher3.tapl; +import static android.view.KeyEvent.KEYCODE_ESCAPE; + +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL; + import androidx.annotation.NonNull; import androidx.test.uiautomator.UiObject2; +import com.android.launcher3.testing.shared.TestProtocol; + +import java.util.regex.Pattern; + /** * View containing select mode buttons */ @@ -26,9 +34,14 @@ public class SelectModeButtons { private final UiObject2 mSelectModeButtons; private final LauncherInstrumentation mLauncher; - SelectModeButtons(UiObject2 selectModeButtons, - LauncherInstrumentation launcherInstrumentation) { - mSelectModeButtons = selectModeButtons; + private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile( + "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0"); + private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile( + "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0"); + + + SelectModeButtons(LauncherInstrumentation launcherInstrumentation) { + mSelectModeButtons = launcherInstrumentation.waitForLauncherObject("select_mode_buttons"); mLauncher = launcherInstrumentation; } @@ -48,4 +61,25 @@ public class SelectModeButtons { } } } + + /** + * Close select mode when ESC key is pressed. + * @return The Overview + */ + @NonNull + public Overview dismissByEscKey() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN); + mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP); + mLauncher.runToState( + () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE), + OVERVIEW_STATE_ORDINAL, + "pressing Esc"); + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "pressed esc key")) { + return new Overview(mLauncher); + } + } + } + } diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java index a202c53375..e6315f3d3b 100644 --- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java +++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java @@ -153,7 +153,7 @@ public final class Taskbar { return By.clazz(TextView.class).text(""); } - private Rect getVisibleBounds() { + public Rect getVisibleBounds() { return mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID).getVisibleBounds(); } diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java index 064f80cfcc..d05c112d76 100644 --- a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java @@ -68,6 +68,7 @@ public final class TaskbarAppIcon extends AppIcon implements SplitscreenDragSour @Override protected boolean launcherStopsAfterLaunch() { + // false because if taskbar is showing then launcher is already stopped. return false; } } diff --git a/tests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java b/tests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java index ec1cbd8ec1..3895302794 100644 --- a/tests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java +++ b/tests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java @@ -16,6 +16,7 @@ package com.android.launcher3.tapl; import static com.android.launcher3.tapl.Launchable.DEFAULT_DRAG_STEPS; + import static org.junit.Assert.assertTrue; import android.graphics.Point; @@ -56,16 +57,20 @@ public class WidgetResizeFrame { Rect originalWidgetSize = widget.getVisibleBounds(); Point targetStart = bottomResizeHandle.getVisibleCenter(); Point targetDest = bottomResizeHandle.getVisibleCenter(); - targetDest.offset(0, originalWidgetSize.height()); + targetDest.offset(0, + originalWidgetSize.height() + mLauncher.getCellLayoutBoarderHeight()); final long downTime = SystemClock.uptimeMillis(); mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetStart, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); - mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS, - true, downTime, downTime, true, - LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); - mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest, - LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); + try { + mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS, + true, downTime, downTime, true, + LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); + } finally { + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest, + LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); + } try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer( "want to return resized widget resize frame")) { diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 105bc3bf50..6387b05cc8 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -19,6 +19,7 @@ package com.android.launcher3.tapl; import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; import static com.android.launcher3.tapl.LauncherInstrumentation.log; +import android.annotation.Nullable; import android.graphics.Rect; import androidx.test.uiautomator.By; @@ -114,7 +115,13 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + /** Get widget with supplied text. */ public Widget getWidget(String labelText) { + return getWidget(labelText, null); + } + + /** Get widget with supplied text and app package */ + public Widget getWidget(String labelText, @Nullable String testAppWidgetPackage) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "getting widget " + labelText + " in widgets list")) { @@ -124,7 +131,8 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer mLauncher.assertTrue("Widgets container didn't become scrollable", fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS)); - final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(); + final UiObject2 widgetsContainer = + findTestAppWidgetsTableContainer(testAppWidgetPackage); mLauncher.assertTrue("Can't locate widgets list for the test app: " + mLauncher.getLauncherPackageName(), widgetsContainer != null); @@ -180,14 +188,22 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer return searchBar; } - /** Finds the widgets list of this test app from the collapsed full widgets picker. */ - private UiObject2 findTestAppWidgetsTableContainer() { + /** + * Finds the widgets list of this test app or supplied test app package from the collapsed full + * widgets picker. + */ + private UiObject2 findTestAppWidgetsTableContainer(@Nullable String testAppWidgetPackage) { final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(), "widgets_list_header"); final BySelector widgetPickerSelector = By.res(mLauncher.getLauncherPackageName(), "container"); - final BySelector targetAppSelector = By.clazz("android.widget.TextView").text( - mLauncher.getContext().getPackageName()); + + String packageName = mLauncher.getContext().getPackageName(); + final BySelector targetAppSelector = By + .clazz("android.widget.TextView") + .text((testAppWidgetPackage == null || testAppWidgetPackage.isEmpty()) + ? packageName + : testAppWidgetPackage); final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(), "widgets_table"); diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index f8fa00c365..4e926341b6 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -17,10 +17,16 @@ package com.android.launcher3.tapl; import static android.view.KeyEvent.KEYCODE_META_RIGHT; +import static android.view.KeyEvent.KEYCODE_RECENT_APPS; +import static android.view.KeyEvent.KEYCODE_TAB; +import static android.view.KeyEvent.META_META_ON; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED; import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE; +import static com.android.launcher3.testing.shared.TestProtocol.UIOBJECT_STALE_ELEMENT; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; @@ -28,6 +34,7 @@ import static junit.framework.TestCase.assertTrue; import android.graphics.Point; import android.graphics.Rect; import android.os.SystemClock; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -121,7 +128,10 @@ public final class Workspace extends Home { LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to open all apps search")) { verifyActiveContainer(); - mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT); + mLauncher.runToState( + () -> mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT), + ALL_APPS_STATE_ORDINAL, + "pressing keyboard shortcut"); try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "pressed meta key")) { return new HomeAllApps(mLauncher); @@ -129,6 +139,40 @@ public final class Workspace extends Home { } } + /** Opens the Launcher Overview page with the action+tab keyboard shortcut. */ + public Overview openOverviewFromActionPlusTabKeyboardShortcut() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = + mLauncher.addContextLayer("want to open overview")) { + verifyActiveContainer(); + mLauncher.runToState( + () -> mLauncher.getDevice().pressKeyCode(KEYCODE_TAB, META_META_ON), + OVERVIEW_STATE_ORDINAL, + "pressing keyboard shortcut"); + try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( + "pressed meta+tab key")) { + return new Overview(mLauncher); + } + } + } + + /** Opens the Launcher Overview page with the Recents keyboard shortcut. */ + public Overview openOverviewFromRecentsKeyboardShortcut() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = + mLauncher.addContextLayer("want to open overview")) { + verifyActiveContainer(); + mLauncher.runToState( + () -> mLauncher.getDevice().pressKeyCode(KEYCODE_RECENT_APPS), + OVERVIEW_STATE_ORDINAL, + "pressing keyboard shortcut"); + try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( + "pressed recents apps key")) { + return new Overview(mLauncher); + } + } + } + /** * Returns the home qsb. * @@ -192,16 +236,24 @@ public final class Workspace extends Home { } /** - * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the - * second screen. + * Ensures that workspace is scrollable. If it's not, drags a chrome app icon from hotseat + * to the second screen. */ public void ensureWorkspaceIsScrollable() { + ensureWorkspaceIsScrollable("Chrome"); + } + + /** + * Ensures that workspace is scrollable. If it's not, drags an icon of a given app name from + * hotseat to the second screen. + */ + public void ensureWorkspaceIsScrollable(String appName) { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { final UiObject2 workspace = verifyActiveContainer(); if (!isWorkspaceScrollable(workspace)) { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "dragging icon to a second page of workspace to make it scrollable")) { - dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen()); + dragIcon(workspace, getHotseatAppIcon(appName), pagesPerScreen()); verifyActiveContainer(); } } @@ -304,8 +356,17 @@ public final class Workspace extends Home { return workspaceIcons.stream() .collect( Collectors.toMap( - /* keyMapper= */ UiObject2::getText, - /* valueMapper= */ UiObject2::getVisibleCenter, + /* keyMapper= */ uiObject21 -> { + Log.d(UIOBJECT_STALE_ELEMENT, "keyText: " + + uiObject21.getText()); + return uiObject21.getText(); + }, + /* valueMapper= */ uiObject2 -> { + Log.d(UIOBJECT_STALE_ELEMENT, uiObject2.getText() + + " dispId" + uiObject2.getDisplayId() + + " parent" + uiObject2.getParent()); + return uiObject2.getVisibleCenter(); + }, /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2)); } @@ -450,7 +511,12 @@ public final class Workspace extends Home { } /** Returns the index of the current page */ - private static int geCurrentPage(LauncherInstrumentation launcher) { + public int getCurrentPage() { + return getCurrentPage(mLauncher); + } + + /** Returns the index of the current page */ + private static int getCurrentPage(LauncherInstrumentation launcher) { return launcher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX).getInt( TestProtocol.TEST_INFO_RESPONSE_FIELD); } @@ -524,7 +590,8 @@ public final class Workspace extends Home { * This function expects the launchable is inside the workspace and there is no drop event. */ static void dragIconToWorkspace( - LauncherInstrumentation launcher, Launchable launchable, Supplier destSupplier) { + LauncherInstrumentation launcher, Launchable launchable, Supplier destSupplier, + boolean isDraggingToFolder) { dragIconToWorkspace( launcher, launchable, @@ -532,7 +599,8 @@ public final class Workspace extends Home { /* isDecelerating= */ false, () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), /* expectDropEvents= */ null, - /* startsActivity = */ false); + /* startsActivity = */ false, + isDraggingToFolder); } static void dragIconToWorkspace( @@ -543,7 +611,8 @@ public final class Workspace extends Home { @Nullable Runnable expectDropEvents, boolean startsActivity) { dragIconToWorkspace(launcher, launchable, dest, /* isDecelerating */ true, - expectLongClickEvents, expectDropEvents, startsActivity); + expectLongClickEvents, expectDropEvents, startsActivity, + /* isDraggingToFolder */ false); } static void dragIconToWorkspace( @@ -553,10 +622,13 @@ public final class Workspace extends Home { boolean isDecelerating, Runnable expectLongClickEvents, @Nullable Runnable expectDropEvents, - boolean startsActivity) { + boolean startsActivity, + boolean isDraggingToFolder) { try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( "want to drag icon to workspace")) { final long downTime = SystemClock.uptimeMillis(); + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "Workspace.dragIconToWorkspace: starting drag | downtime: " + downTime); Point dragStart = launchable.startDrag( downTime, expectLongClickEvents, @@ -580,11 +652,27 @@ public final class Workspace extends Home { dragStart = screenEdge; } - // 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, SystemClock.uptimeMillis(), false, - LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); + // targetDest.x is now between 0 and displayX so we found the target page. + // If not a folder, we just have to put move the icon to the destination and drop it. + // If it's a folder we want to drag to the folder icon and then drag to the center of + // that folder when it opens. + if (isDraggingToFolder) { + Point finalDragStart = dragStart; + Point finalTargetDest = targetDest; + Folder folder = executeAndWaitForFolderOpen(launcher, () -> launcher.movePointer( + finalDragStart, finalTargetDest, DEFAULT_DRAG_STEPS, isDecelerating, + downTime, SystemClock.uptimeMillis(), false, + LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER)); + + Rect dropBounds = folder.getDropLocationBounds(); + dragStart = targetDest; + targetDest = new Point(dropBounds.centerX(), dropBounds.centerY()); + } + + launcher.movePointer(dragStart, targetDest, + DEFAULT_DRAG_STEPS, isDecelerating, downTime, SystemClock.uptimeMillis(), + false, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); + dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, startsActivity); } } @@ -637,7 +725,7 @@ public final class Workspace extends Home { Point currentPosition, int destinationWorkspaceIndex, int y) { final long downTime = SystemClock.uptimeMillis(); int displayX = launcher.getRealDisplaySize().x; - int currentPage = Workspace.geCurrentPage(launcher); + int currentPage = Workspace.getCurrentPage(launcher); int counter = 0; while (currentPage != destinationWorkspaceIndex) { counter++; @@ -656,7 +744,7 @@ public final class Workspace extends Home { () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, true, downTime, downTime, true, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER)); - currentPage = Workspace.geCurrentPage(launcher); + currentPage = Workspace.getCurrentPage(launcher); currentPosition = screenEdge; } return currentPosition; @@ -669,6 +757,16 @@ public final class Workspace extends Home { () -> "Page scroll didn't happen", "Scrolling page"); } + private static Folder executeAndWaitForFolderOpen(LauncherInstrumentation launcher, + Runnable command) { + launcher.executeAndWaitForEvent(command, + event -> TestProtocol.FOLDER_OPENED_MESSAGE.equals( + event.getClassName().toString()), + () -> "Fail to open folder.", + "open folder"); + return new Folder(launcher); + } + static void dragIconToHotseat( LauncherInstrumentation launcher, Launchable launchable, @@ -695,10 +793,9 @@ public final class Workspace extends Home { */ public void flingForward() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - final UiObject2 workspace = verifyActiveContainer(); - mLauncher.scroll(workspace, Direction.RIGHT, - new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0), - FLING_STEPS, false); + Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer()); + mLauncher.pointerScroll( + workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.RIGHT); verifyActiveContainer(); } } @@ -709,10 +806,9 @@ public final class Workspace extends Home { */ public void flingBackward() { try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { - final UiObject2 workspace = verifyActiveContainer(); - mLauncher.scroll(workspace, Direction.LEFT, - new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0), - FLING_STEPS, false); + Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer()); + mLauncher.pointerScroll( + workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.LEFT); verifyActiveContainer(); } } diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java index e5a2a2ef6c..b42d43b0db 100644 --- a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java +++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java @@ -15,7 +15,10 @@ */ package com.android.launcher3.tapl; +import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE; + import android.graphics.Point; +import android.util.Log; import java.util.function.Supplier; @@ -76,6 +79,9 @@ interface WorkspaceDragSource { LauncherInstrumentation.Closable c = launcher.addContextLayer( String.format("want to drag the icon to cell(%d, %d)", cellX, cellY))) { final Supplier dest = () -> Workspace.getCellCenter(launcher, cellX, cellY); + Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE, + "WorkspaceDragSource.dragToWorkspace: dragging icon to workspace | dest: " + + dest.get()); Workspace.dragIconToWorkspace( launcher, launchable,