diff --git a/Android.mk b/Android.mk index 1cf15beeed..d3823c3d5f 100644 --- a/Android.mk +++ b/Android.mk @@ -33,7 +33,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) \ LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/WallpaperPicker/res \ $(LOCAL_PATH)/res \ - $(LOCAL_PATH)/../../../frameworks/support/v7/recyclerview/res + frameworks/support/v7/recyclerview/res LOCAL_PROGUARD_FLAG_FILES := proguard.flags @@ -54,40 +54,20 @@ include $(BUILD_PACKAGE) # -# Protocol Buffer Debug Utility in Java +# Launcher proto buffer jar used for development # include $(CLEAR_VARS) -LOCAL_SRC_FILES := $(call all-java-files-under, util) \ - $(call all-proto-files-under, protos) +LOCAL_SRC_FILES := $(call all-proto-files-under, protos) LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ LOCAL_MODULE_TAGS := optional -LOCAL_MODULE := launcher_protoutil_lib +LOCAL_MODULE := launcher_proto_lib LOCAL_IS_HOST_MODULE := true -LOCAL_JAR_MANIFEST := util/etc/manifest.txt +LOCAL_STATIC_JAVA_LIBRARIES := host-libprotobuf-java-nano include $(BUILD_HOST_JAVA_LIBRARY) -# -# Protocol Buffer Debug Utility Wrapper Script -# -include $(CLEAR_VARS) -LOCAL_IS_HOST_MODULE := true -LOCAL_MODULE_CLASS := EXECUTABLES -LOCAL_MODULE_TAGS := optional -LOCAL_MODULE := launcher_protoutil - -include $(BUILD_SYSTEM)/base_rules.mk - -$(LOCAL_BUILT_MODULE): | $(HOST_OUT_JAVA_LIBRARIES)/launcher_protoutil_lib.jar -$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/util/etc/launcher_protoutil | $(ACP) - @echo "Copy: $(PRIVATE_MODULE) ($@)" - $(copy-file-to-new-target) - $(hide) chmod 755 $@ - -INTERNAL_DALVIK_MODULES += $(LOCAL_INSTALLED_MODULE) - include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 48dcf4ee7f..918ae52aaa 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -81,6 +81,7 @@ android:theme="@style/Theme" android:windowSoftInputMode="adjustPan" android:screenOrientation="nosensor" + android:configChanges="keyboard|keyboardHidden|navigation" android:resumeWhilePausing="true" android:taskAffinity="" android:enabled="true"> @@ -92,39 +93,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/WallpaperPicker/res/anim/fade_out.xml b/WallpaperPicker/res/anim/fade_out.xml new file mode 100644 index 0000000000..2749d9280b --- /dev/null +++ b/WallpaperPicker/res/anim/fade_out.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml b/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml index 8e349b7324..2b39b09527 100644 --- a/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml +++ b/WallpaperPicker/res/layout/actionbar_set_wallpaper.xml @@ -28,5 +28,6 @@ android:drawableLeft="@drawable/ic_actionbar_accept" android:drawablePadding="8dp" android:gravity="start|center_vertical" - android:text="@string/wallpaper_instructions"> + android:text="@string/wallpaper_instructions" + android:enabled="false"> diff --git a/WallpaperPicker/res/values-af/strings.xml b/WallpaperPicker/res/values-af/strings.xml index eb816402a7..bc87fe1213 100644 --- a/WallpaperPicker/res/values-af/strings.xml +++ b/WallpaperPicker/res/values-af/strings.xml @@ -22,6 +22,7 @@ "Stel muurpapier" "Kon nie prent laai nie" "Kon nie prent as muurpapier laai nie" + "Kon nie prent as muurpapier stel nie" "%1$d gekies" "%1$d gekies" diff --git a/WallpaperPicker/res/values-am/strings.xml b/WallpaperPicker/res/values-am/strings.xml index 7e793856ec..23bf538e3c 100644 --- a/WallpaperPicker/res/values-am/strings.xml +++ b/WallpaperPicker/res/values-am/strings.xml @@ -22,6 +22,7 @@ "ልጣፍ አዘጋጅ" "ምስሉን መጫን አልተቻለም" "ምስሉን እንደ ግድግዳ ወረቀት መጫን አልተቻለም" + "ምስሉን እንደ ግድግዳ ወረቀት ማዘጋጀት አልተቻለም" "%1$d ተመርጧል" "%1$d ተመርጧል" diff --git a/WallpaperPicker/res/values-ar/strings.xml b/WallpaperPicker/res/values-ar/strings.xml index 84fa104db2..db6834c9ad 100644 --- a/WallpaperPicker/res/values-ar/strings.xml +++ b/WallpaperPicker/res/values-ar/strings.xml @@ -22,6 +22,7 @@ "تعيين الخلفية" "تعذر تحميل الصورة" "تعذر تحميل الصورة كخلفية" + "تعذر تعيين الصورة كخلفية" "‏تم تحديد %1$d" "‏تم تحديد %1$d" diff --git a/WallpaperPicker/res/values-bg/strings.xml b/WallpaperPicker/res/values-bg/strings.xml index 60ae3022f3..bf2a83b1ee 100644 --- a/WallpaperPicker/res/values-bg/strings.xml +++ b/WallpaperPicker/res/values-bg/strings.xml @@ -22,6 +22,7 @@ "Задаване на тапета" "Изображението не можа да бъде заредено" "Изображението не можа да бъде заредено като тапет" + "Изображението не можа да бъде зададено като тапет" "Избрахте %1$d" "Избрахте %1$d" diff --git a/WallpaperPicker/res/values-bn-rBD/strings.xml b/WallpaperPicker/res/values-bn-rBD/strings.xml index 74d7e57bda..1c4d3d493c 100644 --- a/WallpaperPicker/res/values-bn-rBD/strings.xml +++ b/WallpaperPicker/res/values-bn-rBD/strings.xml @@ -22,6 +22,7 @@ "ওয়ালপেপার সেট করুন" "চিত্র লোড করা যায়নি" "ওয়ালপেপার হিসাবে চিত্র লোড করা যায়নি" + "ওয়ালপেপার হিসাবে চিত্র সেট করা যায়নি" "%1$dটি নির্বাচন করা হয়েছে" "%1$dটি নির্বাচন করা হয়েছে" diff --git a/WallpaperPicker/res/values-ca/strings.xml b/WallpaperPicker/res/values-ca/strings.xml index cbec76250e..9533ae918c 100644 --- a/WallpaperPicker/res/values-ca/strings.xml +++ b/WallpaperPicker/res/values-ca/strings.xml @@ -22,6 +22,7 @@ "Estableix el fons de pantalla" "No s\'ha pogut carregar la imatge." "No s\'ha pogut carregar la imatge com a fons de pantalla." + "No s\'ha pogut definir la imatge com a fons de pantalla" "Seleccionats: %1$d" "Seleccionats: %1$d" diff --git a/WallpaperPicker/res/values-cs/strings.xml b/WallpaperPicker/res/values-cs/strings.xml index da0758e0ab..aab8cc8b34 100644 --- a/WallpaperPicker/res/values-cs/strings.xml +++ b/WallpaperPicker/res/values-cs/strings.xml @@ -22,6 +22,7 @@ "Nastavit jako tapetu" "Obrázek nelze načíst." "Obrázek nelze načíst jako tapetu." + "Obrázek nelze nastavit jako tapetu" "Vybráno: %1$d" "Vybráno: %1$d" diff --git a/WallpaperPicker/res/values-da/strings.xml b/WallpaperPicker/res/values-da/strings.xml index 12c0fe87e6..10ef5b3da0 100644 --- a/WallpaperPicker/res/values-da/strings.xml +++ b/WallpaperPicker/res/values-da/strings.xml @@ -22,6 +22,7 @@ "Angiv baggrund" "Billedet kunne ikke indlæses" "Billedet kunne ikke indlæses som baggrund" + "Billedet kunne ikke indlæses som baggrund" "%1$d er valgt" "%1$d er valgt" diff --git a/WallpaperPicker/res/values-de/strings.xml b/WallpaperPicker/res/values-de/strings.xml index 3171b3f3a7..be35b736bd 100644 --- a/WallpaperPicker/res/values-de/strings.xml +++ b/WallpaperPicker/res/values-de/strings.xml @@ -22,6 +22,7 @@ "Hintergrund auswählen" "Bild konnte nicht geladen werden." "Bild konnte nicht als Hintergrund geladen werden." + "Bild konnte nicht als Hintergrund festgelegt werden." "%1$d ausgewählt" "%1$d ausgewählt" diff --git a/WallpaperPicker/res/values-el/strings.xml b/WallpaperPicker/res/values-el/strings.xml index 2988beced4..1d799a86d5 100644 --- a/WallpaperPicker/res/values-el/strings.xml +++ b/WallpaperPicker/res/values-el/strings.xml @@ -22,6 +22,7 @@ "Ορισμός ταπετσαρίας" "Δεν ήταν δυνατή η φόρτωση της εικόνας" "Δεν ήταν δυνατή η φόρτωση της εικόνας ως ταπετσαρία" + "Δεν ήταν δυνατός ο ορισμός της εικόνας ως ταπετσαρία" "%1$d επιλεγμένα" "%1$d επιλεγμένα" diff --git a/WallpaperPicker/res/values-en-rAU/strings.xml b/WallpaperPicker/res/values-en-rAU/strings.xml index 30450ee814..a384ff6083 100644 --- a/WallpaperPicker/res/values-en-rAU/strings.xml +++ b/WallpaperPicker/res/values-en-rAU/strings.xml @@ -22,6 +22,7 @@ "Set wallpaper" "Couldn\'t load image" "Couldn\'t load image as wallpaper" + "Couldn\'t set image as wallpaper" "%1$d selected" "%1$d selected" diff --git a/WallpaperPicker/res/values-en-rGB/strings.xml b/WallpaperPicker/res/values-en-rGB/strings.xml index 30450ee814..a384ff6083 100644 --- a/WallpaperPicker/res/values-en-rGB/strings.xml +++ b/WallpaperPicker/res/values-en-rGB/strings.xml @@ -22,6 +22,7 @@ "Set wallpaper" "Couldn\'t load image" "Couldn\'t load image as wallpaper" + "Couldn\'t set image as wallpaper" "%1$d selected" "%1$d selected" diff --git a/WallpaperPicker/res/values-en-rIN/strings.xml b/WallpaperPicker/res/values-en-rIN/strings.xml index 30450ee814..a384ff6083 100644 --- a/WallpaperPicker/res/values-en-rIN/strings.xml +++ b/WallpaperPicker/res/values-en-rIN/strings.xml @@ -22,6 +22,7 @@ "Set wallpaper" "Couldn\'t load image" "Couldn\'t load image as wallpaper" + "Couldn\'t set image as wallpaper" "%1$d selected" "%1$d selected" diff --git a/WallpaperPicker/res/values-es-rUS/strings.xml b/WallpaperPicker/res/values-es-rUS/strings.xml index c18ecb2334..924ea56155 100644 --- a/WallpaperPicker/res/values-es-rUS/strings.xml +++ b/WallpaperPicker/res/values-es-rUS/strings.xml @@ -22,6 +22,7 @@ "Establecer como fondo de pantalla" "No se pudo cargar la imagen." "No se pudo cargar la imagen como fondo de pantalla." + "No se pudo establecer la imagen como fondo de pantalla" "%1$d seleccionado" "%1$d seleccionado" diff --git a/WallpaperPicker/res/values-es/strings.xml b/WallpaperPicker/res/values-es/strings.xml index b7221af2ae..8ecd3f4c36 100644 --- a/WallpaperPicker/res/values-es/strings.xml +++ b/WallpaperPicker/res/values-es/strings.xml @@ -22,6 +22,7 @@ "Establecer fondo" "No se ha podido cargar la imagen" "No se ha podido cargar la imagen como fondo de pantalla" + "No se ha podido establecer la imagen como fondo de pantalla" "Seleccionados: %1$d" "Seleccionados: %1$d" diff --git a/WallpaperPicker/res/values-et-rEE/strings.xml b/WallpaperPicker/res/values-et-rEE/strings.xml index 571a0ffbdd..59ca7704c4 100644 --- a/WallpaperPicker/res/values-et-rEE/strings.xml +++ b/WallpaperPicker/res/values-et-rEE/strings.xml @@ -22,6 +22,7 @@ "Määra taustapilt" "Kujutist ei õnnestunud laadida" "Kujutist ei õnnestunud taustapildina laadida" + "Kujutist ei õnnestunud taustapildiks määrata" "Valitud on %1$d" "Valitud on %1$d" diff --git a/WallpaperPicker/res/values-eu-rES/strings.xml b/WallpaperPicker/res/values-eu-rES/strings.xml index 45bf7a1061..fb77b1acd5 100644 --- a/WallpaperPicker/res/values-eu-rES/strings.xml +++ b/WallpaperPicker/res/values-eu-rES/strings.xml @@ -22,6 +22,7 @@ "Ezarri horma-papera" "Ezin izan da irudia kargatu" "Ezin izan da irudia horma-paper gisa kargatu" + "Ezin izan da ezarri irudia horma-paper gisa" "%1$d hautatuta" "%1$d hautatuta" diff --git a/WallpaperPicker/res/values-fa/strings.xml b/WallpaperPicker/res/values-fa/strings.xml index 469ec1263c..da4b7a10d4 100644 --- a/WallpaperPicker/res/values-fa/strings.xml +++ b/WallpaperPicker/res/values-fa/strings.xml @@ -22,6 +22,7 @@ "تنظیم کاغذدیواری" "تصویر بارگیری نشد" "تصویر به‌عنوان کاغذدیواری بارگیری نشد" + "تصویر به‌عنوان کاغذدیواری تنظیم نشد" "‏%1$d انتخاب شد" "‏%1$d انتخاب شد" diff --git a/WallpaperPicker/res/values-fi/strings.xml b/WallpaperPicker/res/values-fi/strings.xml index c82d3e0edd..3c8f1f538c 100644 --- a/WallpaperPicker/res/values-fi/strings.xml +++ b/WallpaperPicker/res/values-fi/strings.xml @@ -22,6 +22,7 @@ "Aseta taustakuva" "Kuvan lataus epäonnistui" "Kuvaa ei voitu ladata taustakuvaksi" + "Kuvan asettaminen taustakuvaksi epäonnistui." "%1$d valittu" "%1$d valittu" diff --git a/WallpaperPicker/res/values-fr-rCA/strings.xml b/WallpaperPicker/res/values-fr-rCA/strings.xml index 92568966ee..ba1d430002 100644 --- a/WallpaperPicker/res/values-fr-rCA/strings.xml +++ b/WallpaperPicker/res/values-fr-rCA/strings.xml @@ -22,6 +22,7 @@ "Définir le fond d\'écran" "Impossible de charger l\'image" "Impossible de charger l\'image comme fond d\'écran" + "Impossible d\'utiliser l\'image comme fond d\'écran" "%1$d sélectionné" "%1$d sélectionné" diff --git a/WallpaperPicker/res/values-fr/strings.xml b/WallpaperPicker/res/values-fr/strings.xml index 479a7e4303..9f3b525311 100644 --- a/WallpaperPicker/res/values-fr/strings.xml +++ b/WallpaperPicker/res/values-fr/strings.xml @@ -22,6 +22,7 @@ "Définir comme fond d\'écran" "Impossible de charger l\'image." "Impossible de charger l\'image comme fond d\'écran." + "Impossible de définir l\'image comme fond d\'écran." "%1$d élément sélectionné" "%1$d élément sélectionné" diff --git a/WallpaperPicker/res/values-gl-rES/strings.xml b/WallpaperPicker/res/values-gl-rES/strings.xml index 0396b2f628..5805489a7d 100644 --- a/WallpaperPicker/res/values-gl-rES/strings.xml +++ b/WallpaperPicker/res/values-gl-rES/strings.xml @@ -22,6 +22,7 @@ "Establecer fondo de pantalla" "Non se puido cargar a imaxe" "Non se puido cargar a imaxe como fondo de pantalla" + "Non se puido definir a imaxe como fondo de pantalla" "Seleccionaches %1$d" "Seleccionaches %1$d" diff --git a/WallpaperPicker/res/values-hi/strings.xml b/WallpaperPicker/res/values-hi/strings.xml index 60834e103d..6f610b5bb5 100644 --- a/WallpaperPicker/res/values-hi/strings.xml +++ b/WallpaperPicker/res/values-hi/strings.xml @@ -22,6 +22,7 @@ "वॉलपेपर सेट करें" "चित्र लोड नहीं किया जा सका" "चित्र को वॉलपेपर के रूप में लोड नहीं किया जा सका" + "चित्र को वॉलपेपर के रूप में सेट नहीं किया जा सका" "%1$d चयनित" "%1$d चयनित" diff --git a/WallpaperPicker/res/values-hr/strings.xml b/WallpaperPicker/res/values-hr/strings.xml index 9ed702c5ab..aaf5e1bcd0 100644 --- a/WallpaperPicker/res/values-hr/strings.xml +++ b/WallpaperPicker/res/values-hr/strings.xml @@ -22,6 +22,7 @@ "Postavi pozadinu" "Nije moguće učitati sliku" "Nije moguće učitati sliku kao pozadinu" + "Postavljanje slike kao pozadine nije uspjelo" "Odabrano je %1$d stavki" "Odabrana je %1$d stavka" diff --git a/WallpaperPicker/res/values-hu/strings.xml b/WallpaperPicker/res/values-hu/strings.xml index d8b08fdfd3..06c0952a40 100644 --- a/WallpaperPicker/res/values-hu/strings.xml +++ b/WallpaperPicker/res/values-hu/strings.xml @@ -22,6 +22,7 @@ "Háttérkép beállítása" "A kép betöltése nem sikerült" "A kép betöltése háttérképként nem sikerült" + "A kép beállítása háttérképként nem sikerült" "%1$d kiválasztva" "%1$d kiválasztva" diff --git a/WallpaperPicker/res/values-hy-rAM/strings.xml b/WallpaperPicker/res/values-hy-rAM/strings.xml index 027b841a73..f68d49f903 100644 --- a/WallpaperPicker/res/values-hy-rAM/strings.xml +++ b/WallpaperPicker/res/values-hy-rAM/strings.xml @@ -22,6 +22,7 @@ "Սահմանել պաստառը" "Չհաջողվեց բեռնել նկարը" "Չհաջողվեց նկարը սահմանել որպես պաստառ" + "Չհաջողվեց նկարը դնել որպես պաստառ" "%1$d ընտրված" "%1$d ընտրված" diff --git a/WallpaperPicker/res/values-in/strings.xml b/WallpaperPicker/res/values-in/strings.xml index 91554522f6..634eb1f765 100644 --- a/WallpaperPicker/res/values-in/strings.xml +++ b/WallpaperPicker/res/values-in/strings.xml @@ -22,6 +22,7 @@ "Setel wallpaper" "Tidak dapat memuat gambar" "Tidak dapat memuat gambar sebagai wallpaper" + "Tidak dapat menyetel gambar sebagai wallpaper" "%1$d dipilih" "%1$d dipilih" diff --git a/WallpaperPicker/res/values-is-rIS/strings.xml b/WallpaperPicker/res/values-is-rIS/strings.xml index 69e6cd4987..eac44ec2ad 100644 --- a/WallpaperPicker/res/values-is-rIS/strings.xml +++ b/WallpaperPicker/res/values-is-rIS/strings.xml @@ -22,6 +22,7 @@ "Velja veggfóður" "Ekki var hægt að hlaða mynd" "Ekki var hægt að hlaða mynd sem veggfóður" + "Ekki var hægt að nota mynd sem veggfóður" "%1$d valin" "%1$d valið" diff --git a/WallpaperPicker/res/values-it/strings.xml b/WallpaperPicker/res/values-it/strings.xml index f4d38d14e0..fdb0ce8de7 100644 --- a/WallpaperPicker/res/values-it/strings.xml +++ b/WallpaperPicker/res/values-it/strings.xml @@ -22,6 +22,7 @@ "Imposta sfondo" "Impossibile caricare l\'immagine" "Impossibile caricare l\'immagine come sfondo" + "Impossibile impostare l\'immagine come sfondo" "%1$d selezionati" "%1$d selezionato" diff --git a/WallpaperPicker/res/values-iw/strings.xml b/WallpaperPicker/res/values-iw/strings.xml index e29f2372f3..c6a583d0e8 100644 --- a/WallpaperPicker/res/values-iw/strings.xml +++ b/WallpaperPicker/res/values-iw/strings.xml @@ -22,6 +22,7 @@ "הגדר טפט" "לא ניתן היה לטעון את התמונה" "לא ניתן היה לטעון את התמונה כטפט" + "לא ניתן היה להגדיר את התמונה כטפט" "‏%1$d נבחרו" "‏%1$d נבחרו" diff --git a/WallpaperPicker/res/values-ja/strings.xml b/WallpaperPicker/res/values-ja/strings.xml index e86026bf95..f18da5d429 100644 --- a/WallpaperPicker/res/values-ja/strings.xml +++ b/WallpaperPicker/res/values-ja/strings.xml @@ -22,6 +22,7 @@ "壁紙を設定" "画像を読み込めませんでした" "画像を壁紙として読み込めませんでした" + "画像を壁紙として設定できませんでした" "%1$d個選択済み" "%1$d個選択済み" diff --git a/WallpaperPicker/res/values-ka-rGE/strings.xml b/WallpaperPicker/res/values-ka-rGE/strings.xml index 1ae10214a2..1f652823f7 100644 --- a/WallpaperPicker/res/values-ka-rGE/strings.xml +++ b/WallpaperPicker/res/values-ka-rGE/strings.xml @@ -22,6 +22,7 @@ "ფონის დაყენება" "სურათი ვერ ჩაიტვირთა." "სურათი ფონად ვერ ჩაიტვირთა." + "სურათი ფონად ვერ დაყენდა" "არჩეულია %1$d" "არჩეულია %1$d" diff --git a/WallpaperPicker/res/values-kk-rKZ/strings.xml b/WallpaperPicker/res/values-kk-rKZ/strings.xml index a10e273189..6f4ee7ca2e 100644 --- a/WallpaperPicker/res/values-kk-rKZ/strings.xml +++ b/WallpaperPicker/res/values-kk-rKZ/strings.xml @@ -22,6 +22,7 @@ "Тұсқағаз орнату" "Суретті жүктей алмады" "Суретті артқы фон ретінде жүктей алмады" + "Кескінді тұсқағаз ретінде орнату мүмкін болмады" "%1$d таңдалған" "%1$d таңдалған" diff --git a/WallpaperPicker/res/values-km-rKH/strings.xml b/WallpaperPicker/res/values-km-rKH/strings.xml index 2a605e86a5..801260d3fa 100644 --- a/WallpaperPicker/res/values-km-rKH/strings.xml +++ b/WallpaperPicker/res/values-km-rKH/strings.xml @@ -22,6 +22,7 @@ "កំណត់​ផ្ទាំង​រូបភាព" "មិន​អាច​ផ្ទុក​រូបភាព" "មិន​អាច​ផ្ទុក​រូបភាព​ជា​ផ្ទាំង​រូបភាព" + "មិនអាចកំណត់រូបភាពជាផ្ទាំងរូបភាពទេ" "បាន​ជ្រើស %1$d" "បាន​ជ្រើស %1$d" diff --git a/WallpaperPicker/res/values-kn-rIN/strings.xml b/WallpaperPicker/res/values-kn-rIN/strings.xml index b69bda5297..7d4d7e70e4 100644 --- a/WallpaperPicker/res/values-kn-rIN/strings.xml +++ b/WallpaperPicker/res/values-kn-rIN/strings.xml @@ -22,6 +22,7 @@ "ವಾಲ್‌ಪೇಪರ್ ಹೊಂದಿಸಿ" "ಚಿತ್ರವನ್ನು ಲೋಡ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ" "ಚಿತ್ರವನ್ನು ವಾಲ್‌ಪೇಪರ್‌ ರೂಪದಲ್ಲಿ ಲೋಡ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ" + "ಚಿತ್ರವನ್ನು ವಾಲ್‌ಪೇಪರ್‌ ರೂಪದಲ್ಲಿ ಹೊಂದಿಸಲಾಗಲಿಲ್ಲ" "%1$d ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" "%1$d ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" diff --git a/WallpaperPicker/res/values-ko/strings.xml b/WallpaperPicker/res/values-ko/strings.xml index 30e7cfa23b..f4072946da 100644 --- a/WallpaperPicker/res/values-ko/strings.xml +++ b/WallpaperPicker/res/values-ko/strings.xml @@ -22,6 +22,7 @@ "배경화면 설정" "이미지를 로드할 수 없습니다." "이미지를 배경화면으로 로드할 수 없습니다." + "이미지를 배경화면으로 설정할 수 없습니다." "%1$d개 선택됨" "%1$d개 선택됨" diff --git a/WallpaperPicker/res/values-ky-rKG/strings.xml b/WallpaperPicker/res/values-ky-rKG/strings.xml index f1ebf095d0..f53f52b29f 100644 --- a/WallpaperPicker/res/values-ky-rKG/strings.xml +++ b/WallpaperPicker/res/values-ky-rKG/strings.xml @@ -22,6 +22,7 @@ "Тушкагаз орнотуу" "Сүрөт жүктөө мүмкүн болбоду" "Сүрөттү тушкагаз катары жүктөө кыйрады" + "Сүрөт тушкагаз катары коюлбай койду" "%1$d тандалды" "%1$d тандалды" diff --git a/WallpaperPicker/res/values-lo-rLA/strings.xml b/WallpaperPicker/res/values-lo-rLA/strings.xml index e32cafb7d7..1da6d29c4a 100644 --- a/WallpaperPicker/res/values-lo-rLA/strings.xml +++ b/WallpaperPicker/res/values-lo-rLA/strings.xml @@ -22,6 +22,7 @@ "ຕັ້ງເປັນພາບພື້ນຫຼັງ" "ບໍ່ສາມາດໂຫຼດຮູບໄດ້" "ບໍ່ສາມາດໂຫຼດຮູບເປັນພາບພື້ນຫຼັງໄດ້" + "ບໍ່ສາມາດໂຫຼດຮູບເປັນພາບພື້ນຫຼັງໄດ້" "ເລືອກ %1$d" "ເລືອກ %1$d" diff --git a/WallpaperPicker/res/values-lt/strings.xml b/WallpaperPicker/res/values-lt/strings.xml index c442e3a5ad..98c9a365c4 100644 --- a/WallpaperPicker/res/values-lt/strings.xml +++ b/WallpaperPicker/res/values-lt/strings.xml @@ -22,6 +22,7 @@ "Nustatyti ekrano foną" "Nepavyko įkelti vaizdo" "Nepavyko įkelti vaizdo kaip ekrano fono" + "Nepavyko nustatyti vaizdo kaip ekrano fono" "Pasirinkta: %1$d" "Pasirinkta: %1$d" diff --git a/WallpaperPicker/res/values-lv/strings.xml b/WallpaperPicker/res/values-lv/strings.xml index d05a175470..ff7876c72e 100644 --- a/WallpaperPicker/res/values-lv/strings.xml +++ b/WallpaperPicker/res/values-lv/strings.xml @@ -22,6 +22,7 @@ "Iestatīt fona tapeti" "Nevarēja ielādēt attēlu." "Nevarēja ielādēt attēlu kā fona tapeti." + "Nevarēja iestatīt attēlu kā fona tapeti." "Atlasīts: %1$d" "Atlasīta: %1$d" diff --git a/WallpaperPicker/res/values-mk-rMK/strings.xml b/WallpaperPicker/res/values-mk-rMK/strings.xml index f9e0963ed0..13b38cd1d5 100644 --- a/WallpaperPicker/res/values-mk-rMK/strings.xml +++ b/WallpaperPicker/res/values-mk-rMK/strings.xml @@ -22,6 +22,7 @@ "Подеси тапет" "Сликата не можеше да се вчита" "Сликата не можеше да се вчита како тапет" + "Сликата не може да се постави како тапет" "Избрано %1$d" "Избрано %1$d" diff --git a/WallpaperPicker/res/values-ml-rIN/strings.xml b/WallpaperPicker/res/values-ml-rIN/strings.xml index 6be771c0ed..5831b36b91 100644 --- a/WallpaperPicker/res/values-ml-rIN/strings.xml +++ b/WallpaperPicker/res/values-ml-rIN/strings.xml @@ -22,6 +22,7 @@ "വാൾപേപ്പർ സജ്ജീകരിക്കുക" "ചിത്രം ലോഡുചെയ്യാനായില്ല" "വാൾപേപ്പറായി ചിത്രം ലോഡുചെയ്യാനായില്ല" + "വാൾപേപ്പറായി ചിത്രം ലോഡുചെയ്യാനായില്ല" "%1$d തിരഞ്ഞെടുത്തു" "%1$d തിരഞ്ഞെടുത്തു" diff --git a/WallpaperPicker/res/values-mn-rMN/strings.xml b/WallpaperPicker/res/values-mn-rMN/strings.xml index b88da5a96e..9995547d55 100644 --- a/WallpaperPicker/res/values-mn-rMN/strings.xml +++ b/WallpaperPicker/res/values-mn-rMN/strings.xml @@ -22,6 +22,7 @@ "Ханын зургийг тохируулах" "Зургийг ачаалж чадсангүй" "Зургийг ханын зураг болгож ачаалж чадсангүй" + "Зургийг ханын зураг болгож чадсангүй" "%1$d сонгогдсон" "%1$d сонгогдсон" diff --git a/WallpaperPicker/res/values-mr-rIN/strings.xml b/WallpaperPicker/res/values-mr-rIN/strings.xml index 128b21c1ca..d740fd29e6 100644 --- a/WallpaperPicker/res/values-mr-rIN/strings.xml +++ b/WallpaperPicker/res/values-mr-rIN/strings.xml @@ -22,6 +22,7 @@ "वॉलपेपर सेट करा" "प्रतिमा लोड करू शकलो नाही" "वॉलपेपर म्हणून प्रतिमा लोड करू शकलो नाही" + "प्रतिमा वॉलपेपर म्हणून सेट करू शकलो नाही" "%1$d निवडले" "%1$d निवडले" diff --git a/WallpaperPicker/res/values-ms-rMY/strings.xml b/WallpaperPicker/res/values-ms-rMY/strings.xml index 9d824e4fa3..759e48cecc 100644 --- a/WallpaperPicker/res/values-ms-rMY/strings.xml +++ b/WallpaperPicker/res/values-ms-rMY/strings.xml @@ -22,6 +22,7 @@ "Tetapkan kertas dinding" "Tidak dapat memuatkan imej" "Tidak dapat memuatkan imej sebagai kertas dinding" + "Tidak dapat menetapkan imej sebagai kertas dinding" "%1$d dipilih" "%1$d dipilih" diff --git a/WallpaperPicker/res/values-my-rMM/strings.xml b/WallpaperPicker/res/values-my-rMM/strings.xml index 233611431d..5197b98ea3 100644 --- a/WallpaperPicker/res/values-my-rMM/strings.xml +++ b/WallpaperPicker/res/values-my-rMM/strings.xml @@ -22,6 +22,7 @@ "နောက်ခံအား သတ်မှတ်ရန်" "ပုံရိပ် တင် မရပါ" "ပုံရိပ်အား နောက်ခံအဖြစ် တင် မရပါ" + "ပုံရိပ်အား နောက်ခံအဖြစ် တင်၍မရပါ" "%1$d ရွေးချယ်ပြီး" "%1$d ရွေးချယ်ပြီး" diff --git a/WallpaperPicker/res/values-nb/strings.xml b/WallpaperPicker/res/values-nb/strings.xml index 9ae0b989a7..8125b53bf0 100644 --- a/WallpaperPicker/res/values-nb/strings.xml +++ b/WallpaperPicker/res/values-nb/strings.xml @@ -22,6 +22,7 @@ "Angi bakgrunn" "Kunne ikke laste inn bildet" "Kunne ikke laste inn bildet som bakgrunn" + "Kunne ikke angi bildet som bakgrunn" "%1$d valgt" "%1$d valgt" diff --git a/WallpaperPicker/res/values-ne-rNP/strings.xml b/WallpaperPicker/res/values-ne-rNP/strings.xml index 5bca8d809d..b77a1c5d7b 100644 --- a/WallpaperPicker/res/values-ne-rNP/strings.xml +++ b/WallpaperPicker/res/values-ne-rNP/strings.xml @@ -22,6 +22,7 @@ "वालपेपर मिलाउनुहोस्" "तस्बिर लोड गर्न सकिएन" "तस्बिरलाई वालपेपरका रूपमा लोड गर्न सकिएन" + "छविलाई वालपेपरको रूपमा सेट गर्न सकिएन" "%1$d चयन भयो" "%1$d चयन भयो" diff --git a/WallpaperPicker/res/values-nl/strings.xml b/WallpaperPicker/res/values-nl/strings.xml index a86f0f6518..dc78305aa3 100644 --- a/WallpaperPicker/res/values-nl/strings.xml +++ b/WallpaperPicker/res/values-nl/strings.xml @@ -22,6 +22,7 @@ "Achtergrond instellen" "Kan afbeelding niet laden" "Kan afbeelding niet laden als achtergrond" + "Kan afbeelding niet instellen als achtergrond" "%1$d geselecteerd" "%1$d geselecteerd" diff --git a/WallpaperPicker/res/values-pl/strings.xml b/WallpaperPicker/res/values-pl/strings.xml index cd52082d6b..9693de46c7 100644 --- a/WallpaperPicker/res/values-pl/strings.xml +++ b/WallpaperPicker/res/values-pl/strings.xml @@ -22,6 +22,7 @@ "Ustaw tapetę" "Nie udało się załadować obrazu" "Nie udało się załadować obrazu jako tapety" + "Nie udało się ustawić obrazu jako tapety" "Wybranych %1$d" "Wybrana %1$d" diff --git a/WallpaperPicker/res/values-pt-rPT/strings.xml b/WallpaperPicker/res/values-pt-rPT/strings.xml index 82aa4694a1..3c4fa9b731 100644 --- a/WallpaperPicker/res/values-pt-rPT/strings.xml +++ b/WallpaperPicker/res/values-pt-rPT/strings.xml @@ -22,6 +22,7 @@ "Definir imagem fundo" "Não foi possível carregar a imagem" "Não foi possível carregar a imagem como imagem de fundo" + "Não foi possível definir a imagem como imagem de fundo" "%1$d selecionadas" "%1$d selecionada" diff --git a/WallpaperPicker/res/values-pt/strings.xml b/WallpaperPicker/res/values-pt/strings.xml index 74ff3106c3..2520eed3a7 100644 --- a/WallpaperPicker/res/values-pt/strings.xml +++ b/WallpaperPicker/res/values-pt/strings.xml @@ -22,6 +22,7 @@ "Definir plano de fundo" "Não foi possível carregar a imagem" "Não foi possível carregar a imagem como plano de fundo" + "Não foi possível definir a imagem como plano de fundo" "%1$d selecionados" "%1$d selecionado" diff --git a/WallpaperPicker/res/values-ro/strings.xml b/WallpaperPicker/res/values-ro/strings.xml index 6281b80fc3..f5df3eee22 100644 --- a/WallpaperPicker/res/values-ro/strings.xml +++ b/WallpaperPicker/res/values-ro/strings.xml @@ -22,6 +22,7 @@ "Setați imaginea de fundal" "Nu s-a putut încărca imaginea" "Nu s-a putut încărca imaginea ca fundal" + "Nu s-a putut seta ca imagine de fundal" "%1$d selectate" "%1$d selectată" diff --git a/WallpaperPicker/res/values-ru/strings.xml b/WallpaperPicker/res/values-ru/strings.xml index 959205a336..f8c350a1f8 100644 --- a/WallpaperPicker/res/values-ru/strings.xml +++ b/WallpaperPicker/res/values-ru/strings.xml @@ -22,6 +22,7 @@ "Установить как обои" "Не удалось загрузить изображение" "Не удалось загрузить изображение" + "Не удалось сменить обои" "Выбрано: %1$d" "Выбрано: %1$d" diff --git a/WallpaperPicker/res/values-si-rLK/strings.xml b/WallpaperPicker/res/values-si-rLK/strings.xml index 37ce1fc528..3945bdf89d 100644 --- a/WallpaperPicker/res/values-si-rLK/strings.xml +++ b/WallpaperPicker/res/values-si-rLK/strings.xml @@ -22,6 +22,7 @@ "වෝල්පේපරය සකසන්න" "පින්තූරය පූරණය කිරීමට නොහැකි විය" "පින්තූරය වෝල්පේපරයක් ලෙස පූරණය කිරීමට නොහැකි විය" + "පින්තූරය බිතුපතක් ලෙස සැකසීමට නොහැකි විය" "%1$d තෝරා ගන්නා ලදි" "%1$d තෝරා ගන්නා ලදි" diff --git a/WallpaperPicker/res/values-sk/strings.xml b/WallpaperPicker/res/values-sk/strings.xml index 9d38de437b..fb2c819b27 100644 --- a/WallpaperPicker/res/values-sk/strings.xml +++ b/WallpaperPicker/res/values-sk/strings.xml @@ -22,6 +22,7 @@ "Nastaviť tapetu" "Obrázok nie je možné načítať" "Obrázok nie je možné načítať ako tapetu" + "Obrázok nie je možné nastaviť ako tapetu" "Počet vybratých položiek: %1$d" "Počet vybratých položiek: %1$d" diff --git a/WallpaperPicker/res/values-sl/strings.xml b/WallpaperPicker/res/values-sl/strings.xml index e776cc7d54..a7ff0891d9 100644 --- a/WallpaperPicker/res/values-sl/strings.xml +++ b/WallpaperPicker/res/values-sl/strings.xml @@ -22,6 +22,7 @@ "Nastavi ozadje" "Slike ni bilo mogoče naložiti" "Slike ni bilo mogoče naložiti kot ozadje" + "Slike ni bilo mogoče nastaviti kot ozadje" "Št. izbranih: %1$d" "Št. izbranih: %1$d" diff --git a/WallpaperPicker/res/values-sr/strings.xml b/WallpaperPicker/res/values-sr/strings.xml index c37bc060f5..6154526cda 100644 --- a/WallpaperPicker/res/values-sr/strings.xml +++ b/WallpaperPicker/res/values-sr/strings.xml @@ -22,6 +22,7 @@ "Подеси позадину" "Није могуће учитати слику" "Није могуће учитати слику као позадину" + "Учитавање слике као позадине није успело" "Изабрано је %1$d" "Изабрана је %1$d" diff --git a/WallpaperPicker/res/values-sv/strings.xml b/WallpaperPicker/res/values-sv/strings.xml index 044508b4da..38062b9eed 100644 --- a/WallpaperPicker/res/values-sv/strings.xml +++ b/WallpaperPicker/res/values-sv/strings.xml @@ -22,6 +22,7 @@ "Ange bakgrund" "Det gick inte att läsa in bilden" "Det gick inte att läsa in bilden som bakgrund" + "Det gick inte att ange bilden som bakgrund" "%1$d har valts" "%1$d har valts" diff --git a/WallpaperPicker/res/values-sw/strings.xml b/WallpaperPicker/res/values-sw/strings.xml index 367912aab7..729a79a74f 100644 --- a/WallpaperPicker/res/values-sw/strings.xml +++ b/WallpaperPicker/res/values-sw/strings.xml @@ -22,6 +22,7 @@ "Weka mandhari" "Haikuweza kupakia picha" "Haikuweza kupakia picha iwe mandhari" + "Haikuweza kuweka picha kuwa mandhari" "%1$d zimechaguliwa" "%1$d zimechaguliwa" diff --git a/WallpaperPicker/res/values-sw720dp-v19/styles.xml b/WallpaperPicker/res/values-sw720dp-v19/styles.xml deleted file mode 100644 index d8dab223a1..0000000000 --- a/WallpaperPicker/res/values-sw720dp-v19/styles.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - diff --git a/WallpaperPicker/res/values-sw720dp/styles.xml b/WallpaperPicker/res/values-sw720dp/styles.xml index 12f8884f18..0058f7e38c 100644 --- a/WallpaperPicker/res/values-sw720dp/styles.xml +++ b/WallpaperPicker/res/values-sw720dp/styles.xml @@ -18,7 +18,11 @@ --> - diff --git a/WallpaperPicker/res/values-ta-rIN/strings.xml b/WallpaperPicker/res/values-ta-rIN/strings.xml index 7284da8ae5..69a9d386be 100644 --- a/WallpaperPicker/res/values-ta-rIN/strings.xml +++ b/WallpaperPicker/res/values-ta-rIN/strings.xml @@ -22,6 +22,7 @@ "வால்பேப்பரை அமை" "படத்தை ஏற்ற முடியவில்லை" "படத்தை வால்பேப்பராக ஏற்ற முடியவில்லை" + "படத்தை வால்பேப்பராக அமைக்க முடியவில்லை" "%1$d தேர்ந்தெடுக்கப்பட்டன" "%1$d தேர்ந்தெடுக்கப்பட்டது" diff --git a/WallpaperPicker/res/values-te-rIN/strings.xml b/WallpaperPicker/res/values-te-rIN/strings.xml index 4c5ad2f080..6fb5fa29b1 100644 --- a/WallpaperPicker/res/values-te-rIN/strings.xml +++ b/WallpaperPicker/res/values-te-rIN/strings.xml @@ -22,6 +22,7 @@ "వాల్‌పేపర్‌ను సెట్ చేయి" "చిత్రాన్ని లోడ్ చేయడం సాధ్యపడలేదు" "చిత్రాన్ని వాల్‌పేపర్‌గా లోడ్ చేయడం సాధ్యపడలేదు" + "చిత్రాన్ని వాల్‌పేపర్‌గా సెట్ చేయడం సాధ్యపడలేదు" "%1$d ఎంచుకోబడింది" "%1$d ఎంచుకోబడింది" diff --git a/WallpaperPicker/res/values-th/strings.xml b/WallpaperPicker/res/values-th/strings.xml index b3d7a8bc7e..c689436060 100644 --- a/WallpaperPicker/res/values-th/strings.xml +++ b/WallpaperPicker/res/values-th/strings.xml @@ -22,6 +22,7 @@ "ตั้งวอลเปเปอร์" "ไม่สามารถโหลดรูปภาพ" "ไม่สามารถโหลดรูปภาพเป็นวอลเปเปอร์" + "ไม่สามารถตั้งรูปภาพเป็นวอลเปเปอร์" "เลือกไว้ %1$d" "เลือกไว้ %1$d" diff --git a/WallpaperPicker/res/values-tl/strings.xml b/WallpaperPicker/res/values-tl/strings.xml index db8f4d5426..c760d7f6e3 100644 --- a/WallpaperPicker/res/values-tl/strings.xml +++ b/WallpaperPicker/res/values-tl/strings.xml @@ -22,6 +22,7 @@ "Itakda ang wallpaper" "Hindi ma-load ang larawan" "Hindi ma-load ang larawan bilang wallpaper" + "Hindi maitakda ang larawan bilang wallpaper" "%1$d ang napili" "%1$d ang napili" diff --git a/WallpaperPicker/res/values-tr/strings.xml b/WallpaperPicker/res/values-tr/strings.xml index d1e57edbbf..e9dc1d2b29 100644 --- a/WallpaperPicker/res/values-tr/strings.xml +++ b/WallpaperPicker/res/values-tr/strings.xml @@ -22,6 +22,7 @@ "Duvar kağıdını ayarla" "Resim yüklenemedi" "Resim duvar kağıdı olarak yüklenemedi" + "Resim, duvar kağıdı olarak ayarlanamadı" "%1$d tane seçildi" "%1$d tane seçildi" diff --git a/WallpaperPicker/res/values-uk/strings.xml b/WallpaperPicker/res/values-uk/strings.xml index 756e7cf092..c6669c0c86 100644 --- a/WallpaperPicker/res/values-uk/strings.xml +++ b/WallpaperPicker/res/values-uk/strings.xml @@ -22,6 +22,7 @@ "Установити фон" "Не вдалося завантажити зображення" "Не вдалося завантажити зображення як фоновий малюнок" + "Не вдалося зробити зображення фоновим малюнком" "Вибрано %1$d" "Вибрано %1$d" diff --git a/WallpaperPicker/res/values-ur-rPK/strings.xml b/WallpaperPicker/res/values-ur-rPK/strings.xml index 86d8163d90..e240e1a50a 100644 --- a/WallpaperPicker/res/values-ur-rPK/strings.xml +++ b/WallpaperPicker/res/values-ur-rPK/strings.xml @@ -22,6 +22,7 @@ "وال پیپر سیٹ کریں" "تصویر کو لوڈ نہیں کیا جا سکا" "تصویر کو وال پیپر کے بطور لوڈ نہیں کیا جا سکا" + "تصویر کو بطور وال پیپر سیٹ نہیں کیا جا سکا" "‏%1$d کو منتخب کیا گیا" "‏%1$d کو منتخب کیا گیا" diff --git a/WallpaperPicker/res/values-uz-rUZ/strings.xml b/WallpaperPicker/res/values-uz-rUZ/strings.xml index 97d3938ae1..5a79981e9d 100644 --- a/WallpaperPicker/res/values-uz-rUZ/strings.xml +++ b/WallpaperPicker/res/values-uz-rUZ/strings.xml @@ -22,6 +22,7 @@ "Fonga rasm o‘rnatish" "Rasm yuklanmadi" "Fon rasmi sifatida rasm yuklanmadi" + "Rasmni fon rasmi sifatida o‘rnatib bo‘lmadi" "%1$d ta tanlandi" "%1$d ta tanlandi" diff --git a/WallpaperPicker/res/values-v21/styles.xml b/WallpaperPicker/res/values-v21/styles.xml index 70220edb9b..d2f7dd44ff 100644 --- a/WallpaperPicker/res/values-v21/styles.xml +++ b/WallpaperPicker/res/values-v21/styles.xml @@ -34,8 +34,11 @@ diff --git a/res/xml/default_workspace_3x3.xml b/res/xml/default_workspace_3x3.xml new file mode 100644 index 0000000000..8d99404504 --- /dev/null +++ b/res/xml/default_workspace_3x3.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/default_workspace_4x4.xml b/res/xml/default_workspace_4x4.xml index 060a1f8802..979a1b4c84 100644 --- a/res/xml/default_workspace_4x4.xml +++ b/res/xml/default_workspace_4x4.xml @@ -23,7 +23,7 @@ + launcher:y="-1" > @@ -31,7 +31,7 @@ + launcher:y="-1" > @@ -39,7 +39,7 @@ + launcher:y="-1" > diff --git a/res/xml/default_workspace_5x5.xml b/res/xml/default_workspace_5x5.xml index 3226617209..f9cc0e789b 100644 --- a/res/xml/default_workspace_5x5.xml +++ b/res/xml/default_workspace_5x5.xml @@ -23,7 +23,7 @@ + launcher:y="-1" > @@ -32,7 +32,7 @@ + launcher:y="-1" > @@ -41,7 +41,7 @@ + launcher:y="-1" > diff --git a/res/xml/default_workspace_5x6.xml b/res/xml/default_workspace_5x6.xml index bc236fb142..8493c265e2 100644 --- a/res/xml/default_workspace_5x6.xml +++ b/res/xml/default_workspace_5x6.xml @@ -23,13 +23,13 @@ + launcher:x="-1" + launcher:y="-1" > diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 3b25dca34c..c4315936cc 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -22,6 +22,8 @@ import android.content.Context; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.FlagOp; +import com.android.launcher3.util.StringFilter; import java.util.ArrayList; import java.util.HashSet; @@ -118,6 +120,21 @@ class AllAppsList { } } + /** + * Updates the apps for the given packageName and user based on {@param op}. + */ + public void updatePackageFlags(StringFilter pkgFilter, UserHandleCompat user, FlagOp op) { + final List data = this.data; + for (int i = data.size() - 1; i >= 0; i--) { + AppInfo info = data.get(i); + final ComponentName component = info.intent.getComponent(); + if (info.user.equals(user) && pkgFilter.matches(component.getPackageName())) { + info.isDisabled = op.apply(info.isDisabled); + modified.add(info); + } + } + } + public void updateIconsAndLabels(HashSet packages, UserHandleCompat user, ArrayList outUpdates) { for (AppInfo info : data) { diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index c95d5585a7..c7a6ae4291 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -26,6 +26,7 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.PackageManagerHelper; import java.util.ArrayList; import java.util.Arrays; @@ -51,11 +52,6 @@ public class AppInfo extends ItemInfo { */ boolean usingLowResIcon; - /** - * The time at which the app was first installed. - */ - long firstInstallTime; - public ComponentName componentName; static final int DOWNLOADED_FLAG = 1; @@ -63,6 +59,11 @@ public class AppInfo extends ItemInfo { int flags = 0; + /** + * {@see ShortcutInfo#isDisabled} + */ + int isDisabled = ShortcutInfo.DEFAULT; + AppInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; } @@ -80,11 +81,22 @@ public class AppInfo extends ItemInfo { */ public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, IconCache iconCache) { + this(context, info, user, iconCache, + UserManagerCompat.getInstance(context).isQuietModeEnabled(user)); + } + + public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, + IconCache iconCache, boolean quietModeEnabled) { this.componentName = info.getComponentName(); this.container = ItemInfo.NO_ID; - flags = initFlags(info); - firstInstallTime = info.getFirstInstallTime(); + if (PackageManagerHelper.isAppSuspended(info.getApplicationInfo())) { + isDisabled |= ShortcutInfo.FLAG_DISABLED_SUSPENDED; + } + if (quietModeEnabled) { + isDisabled |= ShortcutInfo.FLAG_DISABLED_QUIET_USER; + } + iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */); intent = makeLaunchIntent(context, info, user); this.user = user; @@ -109,7 +121,7 @@ public class AppInfo extends ItemInfo { title = Utilities.trim(info.title); intent = new Intent(info.intent); flags = info.flags; - firstInstallTime = info.firstInstallTime; + isDisabled = info.isDisabled; iconBitmap = info.iconBitmap; } @@ -129,7 +141,6 @@ public class AppInfo extends ItemInfo { Log.d(tag, label + " size=" + list.size()); for (AppInfo info: list) { Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap - + " firstInstallTime=" + info.firstInstallTime + " componentName=" + info.componentName.getPackageName()); } } @@ -151,4 +162,9 @@ public class AppInfo extends ItemInfo { .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) .putExtra(EXTRA_PROFILE, serialNumber); } + + @Override + public boolean isDisabled() { + return isDisabled != 0; + } } diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index e6bf525315..6b7ff886c1 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -11,10 +11,15 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; -public class AppWidgetResizeFrame extends FrameLayout { +import com.android.launcher3.accessibility.DragViewStateAnnouncer; +import com.android.launcher3.util.FocusLogic; + +public class AppWidgetResizeFrame extends FrameLayout implements View.OnKeyListener { private static final int SNAP_DURATION = 150; private static final float DIMMED_HANDLE_ALPHA = 0f; private static final float RESIZE_THRESHOLD = 0.66f; @@ -40,6 +45,8 @@ public class AppWidgetResizeFrame extends FrameLayout { private final int[] mLastDirectionVector = new int[2]; private final int[] mTmpPt = new int[2]; + private final DragViewStateAnnouncer mStateAnnouncer; + private boolean mLeftBorderActive; private boolean mRightBorderActive; private boolean mTopBorderActive; @@ -78,6 +85,8 @@ public class AppWidgetResizeFrame extends FrameLayout { mMinHSpan = info.minSpanX; mMinVSpan = info.minSpanY; + mStateAnnouncer = DragViewStateAnnouncer.createFor(this); + setBackgroundResource(R.drawable.widget_resize_shadow); setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); setPadding(0, 0, 0, 0); @@ -137,6 +146,8 @@ public class AppWidgetResizeFrame extends FrameLayout { // cells (same if not resized, or different) will be marked as occupied when the resize // frame is dismissed. mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); + + setOnKeyListener(this); } public boolean beginResizeIfPointInRegion(int x, int y) { @@ -320,12 +331,18 @@ public class AppWidgetResizeFrame extends FrameLayout { if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, mDirectionVector, onDismiss)) { + if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) { + mStateAnnouncer.announce( + mLauncher.getString(R.string.widget_resized, spanX, spanY)); + } + lp.tmpCellX = cellX; lp.tmpCellY = cellY; lp.cellHSpan = spanX; lp.cellVSpan = spanY; mRunningVInc += vSpanDelta; mRunningHInc += hSpanDelta; + if (!onDismiss) { updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); } @@ -462,5 +479,19 @@ public class AppWidgetResizeFrame extends FrameLayout { set.setDuration(SNAP_DURATION); set.start(); } + + setFocusableInTouchMode(true); + requestFocus(); + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // Clear the frame and give focus to the widget host view when a directional key is pressed. + if (FocusLogic.shouldConsume(keyCode)) { + mDragLayer.clearAllResizeFrames(); + mWidgetView.requestFocus(); + return true; + } + return false; } } diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index b1d51ece04..54ce0fd3cc 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -13,7 +13,6 @@ import android.os.AsyncTask; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.compat.AppWidgetManagerCompat; import java.util.ArrayList; import java.util.List; @@ -90,7 +89,7 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { } return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); } LauncherAppState app = LauncherAppState.getInstanceNoCreate(); diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 440e4e7b99..0d71a0c6cf 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -125,8 +125,12 @@ public class AutoInstallsLayout { private static final String ATTR_CLASS_NAME = "className"; private static final String ATTR_TITLE = "title"; private static final String ATTR_SCREEN = "screen"; + + // x and y can be specified as negative integers, in which case -1 represents the + // last row / column, -2 represents the second last, and so on. private static final String ATTR_X = "x"; private static final String ATTR_Y = "y"; + private static final String ATTR_SPAN_X = "spanX"; private static final String ATTR_SPAN_Y = "spanY"; private static final String ATTR_ICON = "icon"; @@ -154,6 +158,8 @@ public class AutoInstallsLayout { protected final int mLayoutId; private final int mHotseatAllAppsRank; + private final int mRowCount; + private final int mColumnCount; private final long[] mTemp = new long[2]; @Thunk final ContentValues mValues; @@ -164,13 +170,6 @@ public class AutoInstallsLayout { public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources res, int layoutId, String rootTag) { - this(context, appWidgetHost, callback, res, layoutId, rootTag, - LauncherAppState.getInstance().getInvariantDeviceProfile().hotseatAllAppsRank); - } - - public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, - LayoutParserCallback callback, Resources res, - int layoutId, String rootTag, int hotseatAllAppsRank) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; @@ -181,7 +180,11 @@ public class AutoInstallsLayout { mSourceRes = res; mLayoutId = layoutId; - mHotseatAllAppsRank = hotseatAllAppsRank; + + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + mHotseatAllAppsRank = idp.hotseatAllAppsRank; + mRowCount = idp.numRows; + mColumnCount = idp.numColumns; } /** @@ -261,8 +264,11 @@ public class AutoInstallsLayout { mValues.put(Favorites.CONTAINER, container); mValues.put(Favorites.SCREEN, screenId); - mValues.put(Favorites.CELLX, getAttributeValue(parser, ATTR_X)); - mValues.put(Favorites.CELLY, getAttributeValue(parser, ATTR_Y)); + + mValues.put(Favorites.CELLX, + convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount)); + mValues.put(Favorites.CELLY, + convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount)); TagParser tagParser = tagParserMap.get(parser.getName()); if (tagParser == null) { @@ -648,6 +654,16 @@ public class AutoInstallsLayout { } } + private static String convertToDistanceFromEnd(String value, int endValue) { + if (!TextUtils.isEmpty(value)) { + int x = Integer.parseInt(value); + if (x < 0) { + return Integer.toString(endValue + x); + } + } + return value; + } + /** * Return attribute value, attempting launcher-specific namespace first * before falling back to anonymous attribute. diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java index c11824054c..538c24a66b 100644 --- a/src/com/android/launcher3/BaseContainerView.java +++ b/src/com/android/launcher3/BaseContainerView.java @@ -17,32 +17,34 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.util.AttributeSet; -import android.util.Log; -import android.widget.LinearLayout; +import android.view.View; +import android.widget.FrameLayout; /** * A base container view, which supports resizing. */ -public abstract class BaseContainerView extends LinearLayout implements Insettable { +public abstract class BaseContainerView extends FrameLayout implements Insettable { private final static String TAG = "BaseContainerView"; // The window insets - private Rect mInsets = new Rect(); - // The bounds of the search bar. Only the left, top, right are used to inset the - // search bar and the height is determined by the measurement of the layout - private Rect mFixedSearchBarBounds = new Rect(); - // The computed bounds of the search bar - private Rect mSearchBarBounds = new Rect(); - // The computed bounds of the container - protected Rect mContentBounds = new Rect(); + private final Rect mInsets = new Rect(); // The computed padding to apply to the container to achieve the container bounds - private Rect mContentPadding = new Rect(); + protected final Rect mContentPadding = new Rect(); // The inset to apply to the edges and between the search bar and the container - private int mContainerBoundsInset; - private boolean mHasSearchBar; + private final int mContainerBoundsInset; + + private final Drawable mRevealDrawable; + + private View mRevealView; + private View mContent; + + protected final int mHorizontalPadding; public BaseContainerView(Context context) { this(context, null); @@ -55,6 +57,30 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.BaseContainerView, defStyleAttr, 0); + mRevealDrawable = a.getDrawable(R.styleable.BaseContainerView_revealBackground); + a.recycle(); + + int maxSize = getResources().getDimensionPixelSize(R.dimen.container_max_width); + int minMargin = getResources().getDimensionPixelSize(R.dimen.container_min_margin); + int width = ((Launcher) context).getDeviceProfile().availableWidthPx; + + if (maxSize > 0) { + mHorizontalPadding = Math.max(minMargin, (width - maxSize) / 2); + } else { + mHorizontalPadding = Math.max(minMargin, + (int) getResources().getFraction(R.fraction.container_margin, width, 1)); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mContent = findViewById(R.id.main_content); + mRevealView = findViewById(R.id.reveal_view); } @Override @@ -63,20 +89,10 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab updateBackgroundAndPaddings(); } - protected void setHasSearchBar() { - mHasSearchBar = true; - } - /** * Sets the search bar bounds for this container view to match. */ final public void setSearchBarBounds(Rect bounds) { - if (LauncherAppState.isDogfoodBuild() && !isValidSearchBarBounds(bounds)) { - Log.e(TAG, "Invalid search bar bounds: " + bounds); - } - - mFixedSearchBarBounds.set(bounds); - // Post the updates since they can trigger a relayout, and this call can be triggered from // a layout pass itself. post(new Runnable() { @@ -92,54 +108,46 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab */ protected void updateBackgroundAndPaddings() { Rect padding; - Rect searchBarBounds = new Rect(); - if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { - // Use the default bounds - padding = new Rect(mInsets.left + mContainerBoundsInset, - (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), - mInsets.right + mContainerBoundsInset, - mInsets.bottom + mContainerBoundsInset); + padding = new Rect( + mHorizontalPadding, + mInsets.top + mContainerBoundsInset, + mHorizontalPadding, + mInsets.bottom + mContainerBoundsInset + ); - // Special case -- we have the search bar, but no specific bounds, so just give it - // the inset bounds without a height. - searchBarBounds.set(mInsets.left + mContainerBoundsInset, - mInsets.top + mContainerBoundsInset, - getMeasuredWidth() - (mInsets.right + mContainerBoundsInset), 0); - } else { - // Use the search bounds, if there is a search bar, the bounds will contain - // the offsets for the insets so we can ignore that - padding = new Rect(mFixedSearchBarBounds.left, - (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), - getMeasuredWidth() - mFixedSearchBarBounds.right, - mInsets.bottom + mContainerBoundsInset); - - // Use the search bounds - searchBarBounds.set(mFixedSearchBarBounds); - } - - // If either the computed container padding has changed, or the computed search bar bounds - // has changed, then notify the container - if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mSearchBarBounds)) { + // The container padding changed, notify the container. + if (!padding.equals(mContentPadding)) { mContentPadding.set(padding); - mContentBounds.set(padding.left, padding.top, - getMeasuredWidth() - padding.right, - getMeasuredHeight() - padding.bottom); - mSearchBarBounds.set(searchBarBounds); - onUpdateBackgroundAndPaddings(mSearchBarBounds, padding); + onUpdateBackgroundAndPaddings(padding); } } - /** - * To be implemented by container views to update themselves when the bounds changes. - */ - protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding); + private void onUpdateBackgroundAndPaddings(Rect padding) { + // Apply the top-bottom padding to itself so that the launcher transition is + // clipped correctly + setPadding(0, padding.top, 0, padding.bottom); - /** - * Returns whether the search bar bounds we got are considered valid. - */ - private boolean isValidSearchBarBounds(Rect searchBarBounds) { - return !searchBarBounds.isEmpty() && - searchBarBounds.right <= getMeasuredWidth() && - searchBarBounds.bottom <= getMeasuredHeight(); + InsetDrawable background = new InsetDrawable(mRevealDrawable, + padding.left, 0, padding.right, 0); + mRevealView.setBackground(background.getConstantState().newDrawable()); + mContent.setBackground(background); + + // We let the content have a intent background, but still have full width. + // This allows the scroll bar to be used responsive outside the background bounds as well. + mContent.setPadding(0, 0, 0, 0); + + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + onUpdateBgPadding(padding, bgPadding); + } + + protected abstract void onUpdateBgPadding(Rect padding, Rect bgPadding); + + public final View getContentView() { + return mContent; + } + + public final View getRevealView() { + return mRevealView; } } \ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index f0d8b3b3d2..f8ef1e1560 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -52,8 +52,8 @@ public abstract class BaseRecyclerView extends RecyclerView public int rowIndex; // The offset of the first visible row public int rowTopOffset; - // The height of a given row (they are currently all the same height) - public int rowHeight; + // The adapter position of the first visible item + public int itemPos; } protected BaseRecyclerViewFastScrollBar mScrollbar; @@ -186,16 +186,22 @@ public abstract class BaseRecyclerView extends RecyclerView return mScrollbar.getThumbMaxWidth(); } + /** + * Returns the visible height of the recycler view: + * VisibleHeight = View height - top padding - bottom padding + */ + protected int getVisibleHeight() { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + return visibleHeight; + } + /** * Returns the available scroll height: * AvailableScrollHeight = Total height of the all items - last page height - * - * This assumes that all rows are the same height. */ - protected int getAvailableScrollHeight(int rowCount, int rowHeight) { - int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom(); - int availableScrollHeight = scrollHeight - visibleHeight; + protected int getAvailableScrollHeight(int rowCount) { + int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom(); + int availableScrollHeight = totalHeight - getVisibleHeight(); return availableScrollHeight; } @@ -204,8 +210,7 @@ public abstract class BaseRecyclerView extends RecyclerView * AvailableScrollBarHeight = Total height of the visible view - thumb height */ protected int getAvailableScrollBarHeight() { - int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight(); return availableScrollBarHeight; } @@ -223,6 +228,13 @@ public abstract class BaseRecyclerView extends RecyclerView return defaultInactiveThumbColor; } + /** + * Returns the scrollbar for this recycler view. + */ + public BaseRecyclerViewFastScrollBar getScrollBar() { + return mScrollbar; + } + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -243,7 +255,7 @@ public abstract class BaseRecyclerView extends RecyclerView int rowCount) { // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); - int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(rowCount); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -252,8 +264,7 @@ public abstract class BaseRecyclerView extends RecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int scrollY = getScrollTop(scrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -267,25 +278,48 @@ public abstract class BaseRecyclerView extends RecyclerView mScrollbar.setThumbOffset(scrollBarX, scrollBarY); } + /** + * @return whether fast scrolling is supported in the current state. + */ + protected boolean supportsFastScrolling() { + return true; + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + *

Override in each subclass of this base class. + * + * @return the scroll top of this recycler view. + */ + protected int getScrollTop(ScrollPositionState scrollPosState) { + return getPaddingTop() + getTop(scrollPosState.rowIndex) - + scrollPosState.rowTopOffset; + } + + /** + * Returns information about the item that the recycler view is currently scrolled to. + */ + protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask); + + /** + * Returns the top (or y position) of the row at the specified index. + */ + protected abstract int getTop(int rowIndex); + /** * Maps the touch (from 0..1) to the adapter position that should be visible. *

Override in each subclass of this base class. */ - public abstract String scrollToPositionAtProgress(float touchFraction); + protected abstract String scrollToPositionAtProgress(float touchFraction); /** * Updates the bounds for the scrollbar. *

Override in each subclass of this base class. */ - public abstract void onUpdateScrollbar(int dy); + protected abstract void onUpdateScrollbar(int dy); /** *

Override in each subclass of this base class. */ - public void onFastScrollCompleted() {} - - /** - * Returns information about the item that the recycler view is currently scrolled to. - */ - protected abstract void getCurScrollState(ScrollPositionState stateOut); + protected void onFastScrollCompleted() {} } \ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index fcee7e8ddf..a6801696a6 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -27,6 +27,7 @@ import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.launcher3.util.Thunk; @@ -37,7 +38,7 @@ import com.android.launcher3.util.Thunk; public class BaseRecyclerViewFastScrollBar { public interface FastScrollFocusableView { - void setFastScrollFocused(boolean focused, boolean animated); + void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated); } private final static int MAX_TRACK_ALPHA = 30; @@ -189,7 +190,8 @@ public class BaseRecyclerViewFastScrollBar { // Check if we should start scrolling, but ignore this fastscroll gesture if we have // exceeded some fixed movement mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop(); - if (!mIsDragging && !mIgnoreDragGesture && isNearThumb(downX, lastY) && + if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() && + isNearThumb(downX, lastY) && Math.abs(y - downY) > config.getScaledTouchSlop()) { mRv.getParent().requestDisallowInterceptTouchEvent(true); mIsDragging = true; @@ -198,7 +200,7 @@ public class BaseRecyclerViewFastScrollBar { } mTouchOffset += (lastY - downY); mPopup.animateVisibility(true); - animateScrollbar(true); + showActiveScrollbar(true); } if (mIsDragging) { // Update the fastscroller section name at this touch position @@ -209,7 +211,7 @@ public class BaseRecyclerViewFastScrollBar { (bottom - top)); mPopup.setSectionName(sectionName); mPopup.animateVisibility(!sectionName.isEmpty()); - mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); + mRv.invalidate(mPopup.updateFastScrollerBounds(lastY)); mLastTouchY = boundedY; } break; @@ -221,7 +223,7 @@ public class BaseRecyclerViewFastScrollBar { if (mIsDragging) { mIsDragging = false; mPopup.animateVisibility(false); - animateScrollbar(false); + showActiveScrollbar(false); } break; } @@ -245,7 +247,7 @@ public class BaseRecyclerViewFastScrollBar { /** * Animates the width and color of the scrollbar. */ - private void animateScrollbar(boolean isScrolling) { + private void showActiveScrollbar(boolean isScrolling) { if (mScrollbarAnimator != null) { mScrollbarAnimator.cancel(); } diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java index aeeb5156da..ebaba18805 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java @@ -77,26 +77,26 @@ public class BaseRecyclerViewFastScrollPopup { * Updates the bounds for the fast scroller. * @return the invalidation rect for this update. */ - public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) { + public Rect updateFastScrollerBounds(int lastTouchY) { mInvalidateRect.set(mBgBounds); if (isVisible()) { // Calculate the dimensions and position of the fast scroller popup - int edgePadding = rv.getMaxScrollbarWidth(); + int edgePadding = mRv.getMaxScrollbarWidth(); int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2; int bgHeight = mBgOriginalSize; int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding)); if (Utilities.isRtl(mRes)) { - mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.left = mRv.getBackgroundPadding().left + (2 * mRv.getMaxScrollbarWidth()); mBgBounds.right = mBgBounds.left + bgWidth; } else { - mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right - - (2 * rv.getMaxScrollbarWidth()); + mBgBounds.right = mRv.getWidth() - mRv.getBackgroundPadding().right - + (2 * mRv.getMaxScrollbarWidth()); mBgBounds.left = mBgBounds.right - bgWidth; } mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight); mBgBounds.top = Math.max(edgePadding, - Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight)); + Math.min(mBgBounds.top, mRv.getHeight() - edgePadding - bgHeight)); mBgBounds.bottom = mBgBounds.top + bgHeight; } else { mBgBounds.setEmpty(); diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 5070878244..09ec60c50f 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; @@ -25,8 +24,9 @@ import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Region; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; @@ -37,13 +37,13 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.widget.TextView; import com.android.launcher3.IconCache.IconLoadRequest; import com.android.launcher3.model.PackageItemInfo; +import java.text.NumberFormat; + /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan * because we want to make the bubble taller than the text and TextView's clip is @@ -63,13 +63,6 @@ public class BubbleTextView extends TextView private static final int DISPLAY_WORKSPACE = 0; private static final int DISPLAY_ALL_APPS = 1; - private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; - private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; - private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; - private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; - private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; - private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; - private final Launcher mLauncher; private Drawable mIcon; private final Drawable mBackground; @@ -93,12 +86,6 @@ public class BubbleTextView extends TextView private boolean mIgnorePressedStateChange; private boolean mDisableRelayout = false; - private ObjectAnimator mFastScrollFocusAnimator; - private Paint mFastScrollFocusBgPaint; - private float mFastScrollFocusFraction; - private boolean mFastScrollFocused; - private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; - private IconLoadRequest mIconLoadRequest; public BubbleTextView(Context context) { @@ -126,7 +113,7 @@ public class BubbleTextView extends TextView if (display == DISPLAY_WORKSPACE) { setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); } else if (display == DISPLAY_ALL_APPS) { - setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); + setTextSize(TypedValue.COMPLEX_UNIT_SP, grid.allAppsIconTextSizeSp); defaultIconSize = grid.allAppsIconSizePx; } @@ -151,13 +138,6 @@ public class BubbleTextView extends TextView setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); } - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) { - mFastScrollFocusBgPaint = new Paint(); - mFastScrollFocusBgPaint.setAntiAlias(true); - mFastScrollFocusBgPaint.setColor( - getResources().getColor(R.color.container_fastscroll_thumb_active_color)); - } - setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } @@ -170,8 +150,9 @@ public class BubbleTextView extends TextView Bitmap b = info.getIcon(iconCache); FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); - iconDrawable.setGhostModeEnabled(info.isDisabled != 0); - + if (info.isDisabled()) { + iconDrawable.setState(FastBitmapDrawable.State.DISABLED); + } setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { setContentDescription(info.contentDescription); @@ -185,7 +166,11 @@ public class BubbleTextView extends TextView } public void applyFromApplicationInfo(AppInfo info) { - setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); + FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(info.iconBitmap); + if (info.isDisabled()) { + iconDrawable.setState(FastBitmapDrawable.State.DISABLED); + } + setIcon(iconDrawable, mIconSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); @@ -210,6 +195,15 @@ public class BubbleTextView extends TextView verifyHighRes(); } + /** + * Used for measurement only, sets some dummy values on this view. + */ + public void applyDummyInfo() { + ColorDrawable d = new ColorDrawable(); + setIcon(mLauncher.resizeIconDrawable(d), mIconSize); + setText(""); + } + /** * Overrides the default long press timeout. */ @@ -259,7 +253,15 @@ public class BubbleTextView extends TextView private void updateIconState() { if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); + FastBitmapDrawable d = (FastBitmapDrawable) mIcon; + if (getTag() instanceof ItemInfo + && ((ItemInfo) getTag()).isDisabled()) { + d.animateState(FastBitmapDrawable.State.DISABLED); + } else if (isPressed() || mStayPressed) { + d.animateState(FastBitmapDrawable.State.PRESSED); + } else { + d.animateState(FastBitmapDrawable.State.NORMAL); + } } } @@ -362,18 +364,7 @@ public class BubbleTextView extends TextView @Override public void draw(Canvas canvas) { if (!mCustomShadowsEnabled) { - // Draw the fast scroll focus bg if we have one - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG && - mFastScrollFocusFraction > 0f) { - DeviceProfile grid = mLauncher.getDeviceProfile(); - int iconCenterX = getScrollX() + (getWidth() / 2); - int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2); - canvas.drawCircle(iconCenterX, iconCenterY, - mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint); - } - super.draw(canvas); - return; } @@ -469,6 +460,11 @@ public class BubbleTextView extends TextView ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? info.getInstallProgress() : 0)) : 100; + setContentDescription(progressLevel > 0 ? + getContext().getString(R.string.app_downloading_title, info.title, + NumberFormat.getPercentInstance().format(progressLevel * 0.01)) : + getContext().getString(R.string.app_waiting_download_title, info.title)); + if (mIcon != null) { final PreloadIconDrawable preloadDrawable; if (mIcon instanceof PreloadIconDrawable) { @@ -533,8 +529,13 @@ public class BubbleTextView extends TextView */ public void reapplyItemInfo(final ItemInfo info) { if (getTag() == info) { + FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL; + if (mIcon instanceof FastBitmapDrawable) { + prevState = ((FastBitmapDrawable) mIcon).getCurrentState(); + } mIconLoadRequest = null; mDisableRelayout = true; + if (info instanceof AppInfo) { applyFromApplicationInfo((AppInfo) info); } else if (info instanceof ShortcutInfo) { @@ -550,6 +551,13 @@ public class BubbleTextView extends TextView } else if (info instanceof PackageItemInfo) { applyFromPackageItemInfo((PackageItemInfo) info); } + + // If we are reapplying over an old icon, then we should update the new icon to the same + // state as the old icon + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setState(prevState); + } + mDisableRelayout = false; } } @@ -583,55 +591,53 @@ public class BubbleTextView extends TextView } } - // Setters & getters for the animation - public void setFastScrollFocus(float fraction) { - mFastScrollFocusFraction = fraction; - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { - setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); - } else { - invalidate(); - } - } - - public float getFastScrollFocus() { - return mFastScrollFocusFraction; - } - @Override - public void setFastScrollFocused(final boolean focused, boolean animated) { - if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { + public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) { + // We can only set the fast scroll focus state on a FastBitmapDrawable + if (!(mIcon instanceof FastBitmapDrawable)) { return; } - if (mFastScrollFocused != focused) { - mFastScrollFocused = focused; - - if (animated) { - // Clean up the previous focus animator - if (mFastScrollFocusAnimator != null) { - mFastScrollFocusAnimator.cancel(); - } - mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", - focused ? 1f : 0f); - if (focused) { - mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); - } else { - mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); - } - mFastScrollFocusAnimator.setDuration(focused ? - FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); - mFastScrollFocusAnimator.start(); - } else { - mFastScrollFocusFraction = focused ? 1f : 0f; + FastBitmapDrawable d = (FastBitmapDrawable) mIcon; + if (animated) { + FastBitmapDrawable.State prevState = d.getCurrentState(); + if (d.animateState(focusState)) { + // If the state was updated, then update the view accordingly + animate().scaleX(focusState.viewScale) + .scaleY(focusState.viewScale) + .setStartDelay(getStartDelayForStateChange(prevState, focusState)) + .setDuration(d.getDurationForStateChange(prevState, focusState)) + .start(); + } + } else { + if (d.setState(focusState)) { + // If the state was updated, then update the view accordingly + animate().cancel(); + setScaleX(focusState.viewScale); + setScaleY(focusState.viewScale); } } } + /** + * Returns the start delay when animating between certain {@link FastBitmapDrawable} states. + */ + private static int getStartDelayForStateChange(final FastBitmapDrawable.State fromState, + final FastBitmapDrawable.State toState) { + switch (toState) { + case NORMAL: + switch (fromState) { + case FAST_SCROLL_HIGHLIGHTED: + return FastBitmapDrawable.FAST_SCROLL_INACTIVE_DURATION / 4; + } + } + return 0; + } + /** * Interface to be implemented by the grand parent to allow click shadow effect. */ - public static interface BubbleTextShadowHandler { + public interface BubbleTextShadowHandler { void setPressedIcon(BubbleTextView icon, Bitmap background); } } diff --git a/src/com/android/launcher3/BuildInfo.java b/src/com/android/launcher3/BuildInfo.java deleted file mode 100644 index b49ee0d9b8..0000000000 --- a/src/com/android/launcher3/BuildInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.android.launcher3; - -import android.text.TextUtils; -import android.util.Log; - -public class BuildInfo { - private static final boolean DBG = false; - private static final String TAG = "BuildInfo"; - - public boolean isDogfoodBuild() { - return false; - } - - public static BuildInfo loadByName(String className) { - if (TextUtils.isEmpty(className)) return new BuildInfo(); - - if (DBG) Log.d(TAG, "Loading BuildInfo: " + className); - try { - Class cls = Class.forName(className); - return (BuildInfo) cls.newInstance(); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Bad BuildInfo class", e); - } catch (InstantiationException e) { - Log.e(TAG, "Bad BuildInfo class", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Bad BuildInfo class", e); - } catch (ClassCastException e) { - Log.e(TAG, "Bad BuildInfo class", e); - } - return new BuildInfo(); - } -} diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 2baa6d8eed..40a4678dec 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -34,6 +34,7 @@ import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.TextView; @@ -123,6 +124,10 @@ public abstract class ButtonDropTarget extends TextView mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); setTextColor(mHoverColor); } + if (d.stateAnnouncer != null) { + d.stateAnnouncer.cancel(); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } @Override @@ -301,14 +306,10 @@ public abstract class ButtonDropTarget extends TextView setOnClickListener(enable ? this : null); } - protected String getAccessibilityDropConfirmation() { - return null; - } - @Override public void onClick(View v) { LauncherAppState.getInstance().getAccessibilityDelegate() - .handleAccessibleDrop(this, null, getAccessibilityDropConfirmation()); + .handleAccessibleDrop(this, null, null); } public int getTextColor() { diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 84e2d49c23..94e3e41419 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -25,7 +25,6 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -53,6 +52,7 @@ import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; import com.android.launcher3.accessibility.FolderAccessibilityHelper; import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; +import com.android.launcher3.util.ParcelableSparseArray; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -66,7 +66,8 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { public static final int WORKSPACE_ACCESSIBILITY_DRAG = 2; public static final int FOLDER_ACCESSIBILITY_DRAG = 1; - static final String TAG = "CellLayout"; + private static final String TAG = "CellLayout"; + private static final boolean LOGD = false; private Launcher mLauncher; @Thunk int mCellWidth; @@ -84,6 +85,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { private int mMaxGap; private boolean mDropPending = false; private boolean mIsDragTarget = true; + private boolean mJailContent = true; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. @@ -187,7 +189,6 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mLauncher = (Launcher) context; DeviceProfile grid = mLauncher.getDeviceProfile(); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); mCellWidth = mCellHeight = -1; mFixedCellWidth = mFixedCellHeight = -1; @@ -201,10 +202,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mPreviousReorderDirection[0] = INVALID_DIRECTION; mPreviousReorderDirection[1] = INVALID_DIRECTION; - a.recycle(); - setAlwaysDrawnWithCacheEnabled(false); - final Resources res = getResources(); mHotseatScale = (float) grid.hotseatIconSizePx / grid.iconSizePx; @@ -244,9 +242,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { // If an animation is started and then stopped very quickly, we can still // get spurious updates we've cleared the tag. Guard against this. if (outline == null) { - @SuppressWarnings("all") // suppress dead code warning - final boolean debug = false; - if (debug) { + if (LOGD) { Object val = animation.getAnimatedValue(); Log.d(TAG, "anim " + thisIndex + " update: " + val + ", isStopped " + anim.isStopped()); @@ -424,10 +420,36 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { } } - boolean getIsDragOverlapping() { + public boolean getIsDragOverlapping() { return mIsDragOverlapping; } + public void disableJailContent() { + mJailContent = false; + } + + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + if (mJailContent) { + ParcelableSparseArray jail = getJailedArray(container); + super.dispatchSaveInstanceState(jail); + container.put(R.id.cell_layout_jail_id, jail); + } else { + super.dispatchSaveInstanceState(container); + } + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + super.dispatchRestoreInstanceState(mJailContent ? getJailedArray(container) : container); + } + + private ParcelableSparseArray getJailedArray(SparseArray container) { + final Parcelable parcelable = container.get(R.id.cell_layout_jail_id); + return parcelable instanceof ParcelableSparseArray ? + (ParcelableSparseArray) parcelable : new ParcelableSparseArray(); + } + @Override protected void onDraw(Canvas canvas) { if (!mIsDragTarget) { @@ -630,6 +652,9 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; child.setId(childId); + if (LOGD) { + Log.d(TAG, "Adding view to ShortcutsAndWidgetsContainer: " + child); + } mShortcutsAndWidgets.addView(child, index, lp); if (markCells) markCellsAsOccupiedForView(child); @@ -873,9 +898,12 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - int offset = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - - (mCountX * mCellWidth); - int left = getPaddingLeft() + (int) Math.ceil(offset / 2f); + boolean isFullscreen = mShortcutsAndWidgets.getChildCount() > 0 && + ((LayoutParams) mShortcutsAndWidgets.getChildAt(0).getLayoutParams()).isFullscreen; + int left = getPaddingLeft(); + if (!isFullscreen) { + left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f); + } int top = getPaddingTop(); mTouchFeedbackView.layout(left, top, @@ -886,6 +914,15 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { top + b - t); } + /** + * Returns the amount of space left over after subtracting padding and cells. This space will be + * very small, a few pixels at most, and is a result of rounding down when calculating the cell + * width in {@link DeviceProfile#calculateCellWidth(int, int)}. + */ + public int getUnusedHorizontalSpace() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth); + } + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); @@ -1018,8 +1055,8 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { return false; } - void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX, - int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) { + void visualizeDropLocation(View v, Bitmap dragOutline, int cellX, int cellY, int spanX, + int spanY, boolean resize, DropTarget.DragObject dragObject) { final int oldDragCellX = mDragCell[0]; final int oldDragCellY = mDragCell[1]; @@ -1028,6 +1065,9 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { } if (cellX != oldDragCellX || cellY != oldDragCellY) { + Point dragOffset = dragObject.dragView.getDragVisualizeOffset(); + Rect dragRegion = dragObject.dragView.getDragRegion(); + mDragCell[0] = cellX; mDragCell[1] = cellY; // Find the top left corner of the rect the object will occupy @@ -1079,6 +1119,18 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); mDragOutlineAnims[mDragOutlineCurrent].animateIn(); + + if (dragObject.stateAnnouncer != null) { + String msg; + if (isHotseat()) { + msg = getContext().getString(R.string.move_to_hotseat_position, + Math.max(cellX, cellY) + 1); + } else { + msg = getContext().getString(R.string.move_to_empty_cell, + cellY + 1, cellX + 1); + } + dragObject.stateAnnouncer.announce(msg); + } } } @@ -2133,8 +2185,15 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { } ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f); a = va; - va.setRepeatMode(ValueAnimator.REVERSE); - va.setRepeatCount(ValueAnimator.INFINITE); + + // 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 (!Utilities.isPowerSaverOn(getContext())) { + va.setRepeatMode(ValueAnimator.REVERSE); + va.setRepeatCount(ValueAnimator.INFINITE); + } + va.setDuration(mode == MODE_HINT ? HINT_DURATION : PREVIEW_DURATION); va.setStartDelay((int) (Math.random() * 60)); va.addUpdateListener(new AnimatorUpdateListener() { @@ -2655,6 +2714,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.dropped = true; child.requestLayout(); + markCellsAsOccupiedForView(child); } } diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java index 5314ecff17..001e044f9e 100644 --- a/src/com/android/launcher3/CommonAppTypeParser.java +++ b/src/com/android/launcher3/CommonAppTypeParser.java @@ -25,7 +25,7 @@ import android.util.Log; import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.backup.BackupProtos.Favorite; +import com.android.launcher3.backup.nano.BackupProtos.Favorite; import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParserException; @@ -93,7 +93,7 @@ public class CommonAppTypeParser implements LayoutParserCallback { public MyLayoutParser() { super(CommonAppTypeParser.this.mContext, null, CommonAppTypeParser.this, - CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE, 0); + CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE); } @Override diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index 7b91c675be..2bba380062 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -48,9 +48,8 @@ public class DefaultLayoutParser extends AutoInstallsLayout { } public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, - LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag, - int hotseatAllAppsRank) { - super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag, hotseatAllAppsRank); + LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag) { + super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag); } @Override diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 9c8659c293..edaf525d65 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -19,7 +19,6 @@ package com.android.launcher3; import android.animation.TimeInterpolator; import android.content.Context; import android.graphics.PointF; -import android.os.AsyncTask; import android.util.AttributeSet; import android.view.View; import android.view.animation.AnimationUtils; @@ -67,44 +66,14 @@ public class DeleteDropTarget extends ButtonDropTarget { /** * Removes the item from the workspace. If the view is not null, it also removes the view. - * @return true if the item was removed. */ - public static boolean removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) { - if (item instanceof ShortcutInfo) { - LauncherModel.deleteItemFromDatabase(launcher, item); - } else if (item instanceof FolderInfo) { - FolderInfo folder = (FolderInfo) item; - launcher.removeFolder(folder); - LauncherModel.deleteFolderContentsFromDatabase(launcher, folder); - } else if (item instanceof LauncherAppWidgetInfo) { - final LauncherAppWidgetInfo widget = (LauncherAppWidgetInfo) item; - - // Remove the widget from the workspace - launcher.removeAppWidget(widget); - LauncherModel.deleteItemFromDatabase(launcher, widget); - - final LauncherAppWidgetHost appWidgetHost = launcher.getAppWidgetHost(); - - if (appWidgetHost != null && !widget.isCustomWidget() - && widget.isWidgetIdValid()) { - // Deleting an app widget ID is a void call but writes to disk before returning - // to the caller... - new AsyncTask() { - public Void doInBackground(Void ... args) { - appWidgetHost.deleteAppWidgetId(widget.appWidgetId); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } else { - return false; - } - - if (view != null) { - launcher.getWorkspace().removeWorkspaceItem(view); - launcher.getWorkspace().stripEmptyScreens(); - } - return true; + public static void removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) { + // Remove the item from launcher and the db, we can ignore the containerInfo in this call + // because we already remove the drag view from the folder (if the drag originated from + // a folder) in Folder.beginDrag() + launcher.removeItem(view, item, true /* deleteFromDb */); + launcher.getWorkspace().stripEmptyScreens(); + launcher.getDragLayer().announceForAccessibility(launcher.getString(R.string.item_removed)); } @Override @@ -156,9 +125,4 @@ public class DeleteDropTarget extends ButtonDropTarget { dragLayer.animateView(d.dragView, fling, duration, tInterpolator, onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); } - - @Override - protected String getAccessibilityDropConfirmation() { - return getResources().getString(R.string.item_removed); - } } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 774594fe2c..380c6b148b 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -49,6 +49,12 @@ public class DeviceProfile { public final int heightPx; public final int availableWidthPx; public final int availableHeightPx; + /** + * The maximum amount of left/right workspace padding as a percentage of the screen width. + * To be clear, this means that up to 7% of the screen width can be used as left padding, and + * 7% of the screen width can be used as right padding. + */ + private static final float MAX_HORIZONTAL_PADDING_PERCENT = 0.14f; // Overview mode private final int overviewModeMinIconZoneHeightPx; @@ -84,18 +90,25 @@ public class DeviceProfile { public int hotseatCellWidthPx; public int hotseatCellHeightPx; public int hotseatIconSizePx; - private int hotseatBarHeightPx; + private int normalHotseatBarHeightPx, shortHotseatBarHeightPx; + private int hotseatBarHeightPx; // One of the above. // All apps public int allAppsNumCols; public int allAppsNumPredictiveCols; public int allAppsButtonVisualSize; public final int allAppsIconSizePx; - public final int allAppsIconTextSizePx; + public final float allAppsIconTextSizeSp; // QSB - private int searchBarSpaceWidthPx; - private int searchBarSpaceHeightPx; + private int searchBarWidgetInternalPaddingTop, searchBarWidgetInternalPaddingBottom; + private int searchBarTopPaddingPx; + private int tallSearchBarNegativeTopPaddingPx, normalSearchBarTopExtraPaddingPx; + private int searchBarTopExtraPaddingPx; // One of the above. + private int normalSearchBarBottomPaddingPx, tallSearchBarBottomPaddingPx; + private int searchBarBottomPaddingPx; // One of the above. + private int normalSearchBarSpaceHeightPx, tallSearchBarSpaceHeightPx; + private int searchBarSpaceHeightPx; // One of the above. public DeviceProfile(Context context, InvariantDeviceProfile inv, Point minSize, Point maxSize, @@ -139,7 +152,7 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); // AllApps uses the original non-scaled icon text size - allAppsIconTextSizePx = Utilities.pxFromDp(inv.iconTextSize, dm); + allAppsIconTextSizeSp = inv.iconTextSize; // AllApps uses the original non-scaled icon size allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm); @@ -167,7 +180,8 @@ public class DeviceProfile { private void computeAllAppsButtonSize(Context context) { Resources res = context.getResources(); float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f; - allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)); + allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)) - context.getResources() + .getDimensionPixelSize(R.dimen.all_apps_button_scale_down); } private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { @@ -196,10 +210,31 @@ public class DeviceProfile { hotseatIconSizePx = (int) (Utilities.pxFromDp(inv.hotseatIconSize, dm) * scale); // Search Bar - searchBarSpaceWidthPx = Math.min(widthPx, - res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); - searchBarSpaceHeightPx = getSearchBarTopOffset() - + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + normalSearchBarSpaceHeightPx = res.getDimensionPixelSize( + R.dimen.dynamic_grid_search_bar_height); + tallSearchBarSpaceHeightPx = res.getDimensionPixelSize( + R.dimen.dynamic_grid_search_bar_height_tall); + searchBarWidgetInternalPaddingTop = res.getDimensionPixelSize( + R.dimen.qsb_internal_padding_top); + searchBarWidgetInternalPaddingBottom = res.getDimensionPixelSize( + R.dimen.qsb_internal_padding_bottom); + normalSearchBarTopExtraPaddingPx = res.getDimensionPixelSize( + R.dimen.dynamic_grid_search_bar_extra_top_padding); + tallSearchBarNegativeTopPaddingPx = res.getDimensionPixelSize( + R.dimen.dynamic_grid_search_bar_negative_top_padding_short); + if (isTablet && !isVerticalBarLayout()) { + searchBarTopPaddingPx = searchBarWidgetInternalPaddingTop; + normalSearchBarBottomPaddingPx = searchBarWidgetInternalPaddingBottom + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_bottom_padding_tablet); + tallSearchBarBottomPaddingPx = normalSearchBarBottomPaddingPx; + } else { + searchBarTopPaddingPx = searchBarWidgetInternalPaddingTop; + normalSearchBarBottomPaddingPx = searchBarWidgetInternalPaddingBottom + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_bottom_padding); + tallSearchBarBottomPaddingPx = searchBarWidgetInternalPaddingBottom + + res.getDimensionPixelSize( + R.dimen.dynamic_grid_search_bar_bottom_negative_padding_short); + } // Calculate the actual text height Paint textPaint = new Paint(); @@ -211,12 +246,16 @@ public class DeviceProfile { dragViewScale = (iconSizePx + scaleDps) / iconSizePx; // Hotseat - hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; + normalHotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; + shortHotseatBarHeightPx = iconSizePx + 2 * edgeMarginPx; hotseatCellWidthPx = iconSizePx; hotseatCellHeightPx = iconSizePx; // Folder - folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx; + int folderCellPadding = isTablet || isLandscape ? 6 * edgeMarginPx : 3 * edgeMarginPx; + // Don't let the folder get too close to the edges of the screen. + folderCellWidthPx = Math.min(cellWidthPx + folderCellPadding, + (availableWidthPx - 4 * edgeMarginPx) / inv.numFolderColumns); folderCellHeightPx = cellHeightPx + edgeMarginPx; folderBackgroundOffset = -edgeMarginPx; folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; @@ -231,34 +270,42 @@ public class DeviceProfile { int allAppsCellWidthGap = res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; - int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + int numAppsCols = (availableAppsWidthPx + allAppsCellWidthGap - appsViewLeftMarginPx) / (allAppsIconSizePx + allAppsCellWidthGap); int numPredictiveAppCols = Math.max(inv.minAllAppsPredictionColumns, numAppsCols); allAppsNumCols = numAppsCols; allAppsNumPredictiveCols = numPredictiveAppCols; } - /** Returns the search bar top offset */ - private int getSearchBarTopOffset() { - if (isTablet && !isVerticalBarLayout()) { - return 4 * edgeMarginPx; - } else { - return 2 * edgeMarginPx; + /** Returns the amount of extra space to allocate to the search bar for vertical padding. */ + private int getSearchBarTotalVerticalPadding() { + return searchBarTopPaddingPx + searchBarTopExtraPaddingPx + searchBarBottomPaddingPx; + } + + /** Returns the width and height of the search bar, ignoring any padding. */ + public Point getSearchBarDimensForWidgetOpts(Resources res) { + Rect searchBarBounds = getSearchBarBounds(Utilities.isRtl(res)); + if (isVerticalBarLayout()) { + return new Point(searchBarBounds.width(), searchBarBounds.height()); } + int widgetInternalPadding = searchBarWidgetInternalPaddingTop + + searchBarWidgetInternalPaddingBottom; + return new Point(searchBarBounds.width(), searchBarSpaceHeightPx + widgetInternalPadding); } /** Returns the search bar bounds in the current orientation */ public Rect getSearchBarBounds(boolean isLayoutRtl) { Rect bounds = new Rect(); - if (isLandscape && transposeLayoutWithOrientation) { + if (isVerticalBarLayout()) { if (isLayoutRtl) { - bounds.set(availableWidthPx - searchBarSpaceHeightPx, edgeMarginPx, + bounds.set(availableWidthPx - normalSearchBarSpaceHeightPx, edgeMarginPx, availableWidthPx, availableHeightPx - edgeMarginPx); } else { - bounds.set(0, edgeMarginPx, searchBarSpaceHeightPx, + bounds.set(0, edgeMarginPx, normalSearchBarSpaceHeightPx, availableHeightPx - edgeMarginPx); } } else { + int boundsBottom = searchBarSpaceHeightPx + getSearchBarTotalVerticalPadding(); if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons @@ -267,14 +314,13 @@ public class DeviceProfile { // that into account here too. int gap = (int) ((width - 2 * edgeMarginPx - (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1))); - bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(), - availableWidthPx - (edgeMarginPx + gap), - searchBarSpaceHeightPx); + bounds.set(edgeMarginPx + gap, 0, + availableWidthPx - (edgeMarginPx + gap), boundsBottom); } else { bounds.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left, - getSearchBarTopOffset(), + 0, availableWidthPx - (desiredWorkspaceLeftRightMarginPx - - defaultWidgetPadding.right), searchBarSpaceHeightPx); + defaultWidgetPadding.right), boundsBottom); } } return bounds; @@ -284,43 +330,46 @@ public class DeviceProfile { Rect getWorkspacePadding(boolean isLayoutRtl) { Rect searchBarBounds = getSearchBarBounds(isLayoutRtl); Rect padding = new Rect(); - if (isLandscape && transposeLayoutWithOrientation) { + if (isVerticalBarLayout()) { // Pad the left and right of the workspace with search/hotseat bar sizes if (isLayoutRtl) { - padding.set(hotseatBarHeightPx, edgeMarginPx, + padding.set(normalHotseatBarHeightPx, edgeMarginPx, searchBarBounds.width(), edgeMarginPx); } else { padding.set(searchBarBounds.width(), edgeMarginPx, - hotseatBarHeightPx, edgeMarginPx); + normalHotseatBarHeightPx, edgeMarginPx); } } else { + int paddingTop = searchBarBounds.bottom; + int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx; if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons float gapScale = 1f + (dragViewScale - 1f) / 2f; int width = getCurrentWidth(); int height = getCurrentHeight(); - int paddingTop = searchBarBounds.bottom; - int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx; - int availableWidth = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) + - (inv.numColumns * gapScale * cellWidthPx))); - int availableHeight = Math.max(0, height - paddingTop - paddingBottom + // The amount of screen space available for left/right padding. + int availablePaddingX = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) + + ((inv.numColumns - 1) * gapScale * cellWidthPx))); + availablePaddingX = (int) Math.min(availablePaddingX, + width * MAX_HORIZONTAL_PADDING_PERCENT); + int availablePaddingY = Math.max(0, height - paddingTop - paddingBottom - (int) (2 * inv.numRows * cellHeightPx)); - padding.set(availableWidth / 2, paddingTop + availableHeight / 2, - availableWidth / 2, paddingBottom + availableHeight / 2); + padding.set(availablePaddingX / 2, paddingTop + availablePaddingY / 2, + availablePaddingX / 2, paddingBottom + availablePaddingY / 2); } else { // Pad the top and bottom of the workspace with search/hotseat bar sizes padding.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left, - searchBarBounds.bottom, + paddingTop, desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.right, - hotseatBarHeightPx + pageIndicatorHeightPx); + paddingBottom); } } return padding; } private int getWorkspacePageSpacing(boolean isLayoutRtl) { - if ((isLandscape && transposeLayoutWithOrientation) || isLargeTablet) { + if (isVerticalBarLayout() || isLargeTablet) { // In landscape mode the page spacing is set to the default. return defaultPageSpacingPx; } else { @@ -341,7 +390,7 @@ public class DeviceProfile { // The rect returned will be extended to below the system ui that covers the workspace Rect getHotseatRect() { if (isVerticalBarLayout()) { - return new Rect(availableWidthPx - hotseatBarHeightPx, 0, + return new Rect(availableWidthPx - normalHotseatBarHeightPx, 0, Integer.MAX_VALUE, availableHeightPx); } else { return new Rect(0, availableHeightPx - hotseatBarHeightPx, @@ -357,8 +406,9 @@ public class DeviceProfile { } /** - * When {@code true}, hotseat is on the bottom row when in landscape mode. - * If {@code false}, hotseat is on the right column when in landscape mode. + * When {@code true}, the device is in landscape mode and the hotseat is on the right column. + * When {@code false}, either device is in portrait mode or the device is in landscape mode and + * the hotseat is on the bottom row. */ boolean isVerticalBarLayout() { return isLandscape && transposeLayoutWithOrientation; @@ -378,19 +428,38 @@ public class DeviceProfile { return visibleChildren; } + // TODO(twickham): b/25154513 + public void setSearchBarHeight(int searchBarHeight) { + if (searchBarHeight == LauncherCallbacks.SEARCH_BAR_HEIGHT_TALL) { + hotseatBarHeightPx = shortHotseatBarHeightPx; + searchBarSpaceHeightPx = tallSearchBarSpaceHeightPx; + searchBarBottomPaddingPx = tallSearchBarBottomPaddingPx; + searchBarTopExtraPaddingPx = isPhone ? tallSearchBarNegativeTopPaddingPx + : normalSearchBarTopExtraPaddingPx; + } else { + hotseatBarHeightPx = normalHotseatBarHeightPx; + searchBarSpaceHeightPx = normalSearchBarSpaceHeightPx; + searchBarBottomPaddingPx = normalSearchBarBottomPaddingPx; + searchBarTopExtraPaddingPx = normalSearchBarTopExtraPaddingPx; + } + } + public void layout(Launcher launcher) { FrameLayout.LayoutParams lp; boolean hasVerticalBarLayout = isVerticalBarLayout(); final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); // Layout the search bar space + Rect searchBarBounds = getSearchBarBounds(isLayoutRtl); View searchBar = launcher.getSearchDropTargetBar(); lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); + lp.width = searchBarBounds.width(); + lp.height = searchBarBounds.height(); + lp.topMargin = searchBarTopExtraPaddingPx; if (hasVerticalBarLayout) { // Vertical search bar space -- The search bar is fixed in the layout to be on the left // of the screen regardless of RTL lp.gravity = Gravity.LEFT; - lp.width = searchBarSpaceHeightPx; LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); targets.setOrientation(LinearLayout.VERTICAL); @@ -400,11 +469,7 @@ public class DeviceProfile { } else { // Horizontal search bar space - lp.gravity = Gravity.TOP; - lp.height = searchBarSpaceHeightPx; - - LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); - targets.getLayoutParams().width = searchBarSpaceWidthPx; + lp.gravity = Gravity.TOP|Gravity.CENTER_HORIZONTAL; } searchBar.setLayoutParams(lp); @@ -420,11 +485,18 @@ public class DeviceProfile { // Layout the hotseat View hotseat = launcher.findViewById(R.id.hotseat); lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); + // We want the edges of the hotseat to line up with the edges of the workspace, but the + // icons in the hotseat are a different size, and so don't line up perfectly. To account for + // this, we pad the left and right of the hotseat with half of the difference of a workspace + // cell vs a hotseat cell. + float workspaceCellWidth = (float) getCurrentWidth() / inv.numColumns; + float hotseatCellWidth = (float) getCurrentWidth() / inv.numHotseatIcons; + int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2); if (hasVerticalBarLayout) { // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the // screen regardless of RTL lp.gravity = Gravity.RIGHT; - lp.width = hotseatBarHeightPx; + lp.width = normalHotseatBarHeightPx; lp.height = LayoutParams.MATCH_PARENT; hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx); } else if (isTablet) { @@ -432,17 +504,18 @@ public class DeviceProfile { lp.gravity = Gravity.BOTTOM; lp.width = LayoutParams.MATCH_PARENT; lp.height = hotseatBarHeightPx; - hotseat.setPadding(edgeMarginPx + padding.left, 0, - edgeMarginPx + padding.right, - 2 * edgeMarginPx); + hotseat.findViewById(R.id.layout).setPadding( + hotseatAdjustment + padding.left, 0, + hotseatAdjustment + padding.right, 2 * edgeMarginPx); } else { // For phones, layout the hotseat without any bottom margin // to ensure that we have space for the folders lp.gravity = Gravity.BOTTOM; lp.width = LayoutParams.MATCH_PARENT; lp.height = hotseatBarHeightPx; - hotseat.findViewById(R.id.layout).setPadding(2 * edgeMarginPx, 0, - 2 * edgeMarginPx, 0); + hotseat.findViewById(R.id.layout).setPadding( + hotseatAdjustment + padding.left, 0, + hotseatAdjustment + padding.right, 0); } hotseat.setLayoutParams(lp); diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 2191455d53..2f0bfe5c77 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -32,8 +32,10 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; +import com.android.launcher3.accessibility.DragViewStateAnnouncer; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -145,8 +147,6 @@ public class DragController { /** * Used to create a new DragLayer from XML. - * - * @param context The application's context. */ public DragController(Launcher launcher) { Resources r = launcher.getResources(); @@ -239,6 +239,9 @@ public class DragController { mDragObject = new DropTarget.DragObject(); + final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, + registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); + mDragObject.dragComplete = false; if (mIsAccessibleDrag) { // For an accessible drag, we assume the view is being dragged from the center. @@ -248,14 +251,12 @@ public class DragController { } else { mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); } mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; - final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, - registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); - if (dragOffset != null) { dragView.setDragVisualizeOffset(new Point(dragOffset)); } @@ -340,15 +341,15 @@ public class DragController { } endDrag(); } - public void onAppsRemoved(final ArrayList packageNames, HashSet cns) { + + public void onAppsRemoved(final HashSet packageNames, HashSet cns) { // Cancel the current drag if we are removing an app that we are dragging if (mDragObject != null) { Object rawDragInfo = mDragObject.dragInfo; if (rawDragInfo instanceof ShortcutInfo) { ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo; for (ComponentName componentName : cns) { - // Added null checks to prevent NPE we've seen in the wild - if (dragInfo != null && dragInfo.intent != null) { + if (dragInfo.intent != null) { ComponentName cn = dragInfo.intent.getComponent(); boolean isSameComponent = cn != null && (cn.equals(componentName) || packageNames.contains(cn.getPackageName())); diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 1c18747c10..ad9063c25a 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -565,10 +565,6 @@ public class DragLayer extends InsettableFrameLayout { resizeFrame.snapToWidget(false); } - public void animateViewIntoPosition(DragView dragView, final View child) { - animateViewIntoPosition(dragView, child, null, null); - } - public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, int duration) { diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index 2acfc61403..a584667cac 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -321,10 +321,10 @@ public class DragView extends View { setTranslationY(touchY - mRegistrationY); // Post the animation to skip other expensive work happening on the first frame post(new Runnable() { - public void run() { - mAnim.start(); - } - }); + public void run() { + mAnim.start(); + } + }); } public void cancelAnimation() { diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java index c8fac54669..434059168c 100644 --- a/src/com/android/launcher3/DropTarget.java +++ b/src/com/android/launcher3/DropTarget.java @@ -19,6 +19,8 @@ package com.android.launcher3; import android.graphics.PointF; import android.graphics.Rect; +import com.android.launcher3.accessibility.DragViewStateAnnouncer; + /** * Interface defining an object that can receive a drag. * @@ -64,6 +66,8 @@ public interface DropTarget { /** Defers removing the DragView from the DragLayer until after the drop animation. */ public boolean deferDragViewCleanupPostAnimation = true; + public DragViewStateAnnouncer stateAnnouncer; + public DragObject() { } diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index 28e923e67a..d7f1d8677d 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Bitmap; @@ -28,13 +29,40 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.SparseArray; +import android.view.animation.DecelerateInterpolator; public class FastBitmapDrawable extends Drawable { - static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { + /** + * The possible states that a FastBitmapDrawable can be in. + */ + public enum State { + + NORMAL (0f, 0f, 1f, new DecelerateInterpolator()), + PRESSED (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR), + FAST_SCROLL_HIGHLIGHTED (0f, 0f, 1.15f, new DecelerateInterpolator()), + FAST_SCROLL_UNHIGHLIGHTED (0f, 0f, 1f, new DecelerateInterpolator()), + DISABLED (1f, 0.5f, 1f, new DecelerateInterpolator()); + + public final float desaturation; + public final float brightness; + /** + * Used specifically by the view drawing this FastBitmapDrawable. + */ + public final float viewScale; + public final TimeInterpolator interpolator; + + State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) { + this.desaturation = desaturation; + this.brightness = brightness; + this.viewScale = viewScale; + this.interpolator = interpolator; + } + } + + public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @Override public float getInterpolation(float input) { @@ -47,42 +75,46 @@ public class FastBitmapDrawable extends Drawable { } } }; - static final long CLICK_FEEDBACK_DURATION = 2000; + public static final int CLICK_FEEDBACK_DURATION = 2000; + public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225; + public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150; + public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225; + public static final int FAST_SCROLL_INACTIVE_DURATION = 275; - private static final int PRESSED_BRIGHTNESS = 100; - private static ColorMatrix sGhostModeMatrix; - private static final ColorMatrix sTempMatrix = new ColorMatrix(); + // Since we don't need 256^2 values for combinations of both the brightness and saturation, we + // reduce the value space to a smaller value V, which reduces the number of cached + // ColorMatrixColorFilters that we need to keep to V^2 + private static final int REDUCED_FILTER_VALUE_SPACE = 48; - /** - * Store the brightness colors filters to optimize animations during icon press. This - * only works for non-ghost-mode icons. - */ - private static final SparseArray sCachedBrightnessFilter = - new SparseArray(); + // A cache of ColorFilters for optimizing brightness and saturation animations + private static final SparseArray sCachedFilter = new SparseArray<>(); - private static final int GHOST_MODE_MIN_COLOR_RANGE = 130; + // Temporary matrices used for calculation + private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); + private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); private final Bitmap mBitmap; - private int mAlpha; + private State mState = State.NORMAL; + // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and + // as a result, can be used to compose the key for the cached ColorMatrixColorFilters + private int mDesaturation = 0; private int mBrightness = 0; - private boolean mGhostModeEnabled = false; + private int mAlpha = 255; + private int mPrevUpdateKey = Integer.MAX_VALUE; - private boolean mPressed = false; - private ObjectAnimator mPressedAnimator; + // Animators for the fast bitmap drawable's properties + private AnimatorSet mPropertyAnimator; public FastBitmapDrawable(Bitmap b) { - mAlpha = 255; mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); } @Override public void draw(Canvas canvas) { - final Rect r = getBounds(); - // Draw the bitmap into the bounding rect - canvas.drawBitmap(mBitmap, null, r, mPaint); + canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); } @Override @@ -136,96 +168,191 @@ public class FastBitmapDrawable extends Drawable { } /** - * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost' - * appearance. + * Animates this drawable to a new state. + * + * @return whether the state has changed. */ - public void setGhostModeEnabled(boolean enabled) { - if (mGhostModeEnabled != enabled) { - mGhostModeEnabled = enabled; + public boolean animateState(State newState) { + State prevState = mState; + if (mState != newState) { + mState = newState; + + mPropertyAnimator = cancelAnimator(mPropertyAnimator); + mPropertyAnimator = new AnimatorSet(); + mPropertyAnimator.playTogether( + ObjectAnimator + .ofFloat(this, "desaturation", newState.desaturation), + ObjectAnimator + .ofFloat(this, "brightness", newState.brightness)); + mPropertyAnimator.setInterpolator(newState.interpolator); + mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState)); + mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState)); + mPropertyAnimator.start(); + return true; + } + return false; + } + + /** + * Immediately sets this drawable to a new state. + * + * @return whether the state has changed. + */ + public boolean setState(State newState) { + if (mState != newState) { + mState = newState; + + mPropertyAnimator = cancelAnimator(mPropertyAnimator); + + setDesaturation(newState.desaturation); + setBrightness(newState.brightness); + return true; + } + return false; + } + + /** + * Returns the current state. + */ + public State getCurrentState() { + return mState; + } + + /** + * Returns the duration for the state change animation. + */ + public static int getDurationForStateChange(State fromState, State toState) { + switch (toState) { + case NORMAL: + switch (fromState) { + case PRESSED: + return 0; + case FAST_SCROLL_HIGHLIGHTED: + case FAST_SCROLL_UNHIGHLIGHTED: + return FAST_SCROLL_INACTIVE_DURATION; + } + case PRESSED: + return CLICK_FEEDBACK_DURATION; + case FAST_SCROLL_HIGHLIGHTED: + return FAST_SCROLL_HIGHLIGHT_DURATION; + case FAST_SCROLL_UNHIGHLIGHTED: + switch (fromState) { + case NORMAL: + // When animating from normal state, take a little longer + return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION; + default: + return FAST_SCROLL_UNHIGHLIGHT_DURATION; + } + } + return 0; + } + + /** + * Returns the start delay when animating between certain fast scroll states. + */ + public static int getStartDelayForStateChange(State fromState, State toState) { + switch (toState) { + case FAST_SCROLL_UNHIGHLIGHTED: + switch (fromState) { + case NORMAL: + return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4; + } + } + return 0; + } + + /** + * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] + */ + public void setDesaturation(float desaturation) { + int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); + if (mDesaturation != newDesaturation) { + mDesaturation = newDesaturation; updateFilter(); } } - public void setPressed(boolean pressed) { - if (mPressed != pressed) { - mPressed = pressed; - if (mPressed) { - mPressedAnimator = ObjectAnimator - .ofInt(this, "brightness", PRESSED_BRIGHTNESS) - .setDuration(CLICK_FEEDBACK_DURATION); - mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR); - mPressedAnimator.start(); - } else if (mPressedAnimator != null) { - mPressedAnimator.cancel(); - setBrightness(0); - } - } - invalidateSelf(); + public float getDesaturation() { + return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; } - public boolean isGhostModeEnabled() { - return mGhostModeEnabled; - } - - public int getBrightness() { - return mBrightness; - } - - public void setBrightness(int brightness) { - if (mBrightness != brightness) { - mBrightness = brightness; + /** + * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] + */ + public void setBrightness(float brightness) { + int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); + if (mBrightness != newBrightness) { + mBrightness = newBrightness; updateFilter(); - invalidateSelf(); } } + public float getBrightness() { + return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; + } + + /** + * Updates the paint to reflect the current brightness and saturation. + */ private void updateFilter() { - if (mGhostModeEnabled) { - if (sGhostModeMatrix == null) { - sGhostModeMatrix = new ColorMatrix(); - sGhostModeMatrix.setSaturation(0); + boolean usePorterDuffFilter = false; + int key = -1; + if (mDesaturation > 0) { + key = (mDesaturation << 16) | mBrightness; + } else if (mBrightness > 0) { + // Compose a key with a fully saturated icon if we are just animating brightness + key = (1 << 16) | mBrightness; - // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255] - float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f; - sTempMatrix.set(new float[] { - range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE, - 0, 0, 0, 1, 0 }); - sGhostModeMatrix.preConcat(sTempMatrix); - } + // We found that in L, ColorFilters cause drawing artifacts with shadows baked into + // icons, so just use a PorterDuff filter when we aren't animating saturation + usePorterDuffFilter = true; + } - if (mBrightness == 0) { - mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix)); - } else { - setBrightnessMatrix(sTempMatrix, mBrightness); - sTempMatrix.postConcat(sGhostModeMatrix); - mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix)); - } - } else if (mBrightness != 0) { - ColorFilter filter = sCachedBrightnessFilter.get(mBrightness); + // Debounce multiple updates on the same frame + if (key == mPrevUpdateKey) { + return; + } + mPrevUpdateKey = key; + + if (key != -1) { + ColorFilter filter = sCachedFilter.get(key); if (filter == null) { - filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255), - PorterDuff.Mode.SRC_ATOP); - sCachedBrightnessFilter.put(mBrightness, filter); + float brightnessF = getBrightness(); + int brightnessI = (int) (255 * brightnessF); + if (usePorterDuffFilter) { + filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP); + } else { + float saturationF = 1f - getDesaturation(); + sTempFilterMatrix.setSaturation(saturationF); + if (mBrightness > 0) { + // Brightness: C-new = C-old*(1-amount) + amount + float scale = 1f - brightnessF; + float[] mat = sTempBrightnessMatrix.getArray(); + mat[0] = scale; + mat[6] = scale; + mat[12] = scale; + mat[4] = brightnessI; + mat[9] = brightnessI; + mat[14] = brightnessI; + sTempFilterMatrix.preConcat(sTempBrightnessMatrix); + } + filter = new ColorMatrixColorFilter(sTempFilterMatrix); + } + sCachedFilter.append(key, filter); } mPaint.setColorFilter(filter); } else { mPaint.setColorFilter(null); } + invalidateSelf(); } - private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) { - // Brightness: C-new = C-old*(1-amount) + amount - float scale = 1 - brightness / 255.0f; - matrix.setScale(scale, scale, scale, 1); - float[] array = matrix.getArray(); - - // Add the amount to RGB components of the matrix, as per the above formula. - // Fifth elements in the array correspond to the constant being added to - // red, blue, green, and alpha channel respectively. - array[4] = brightness; - array[9] = brightness; - array[14] = brightness; + private AnimatorSet cancelAnimator(AnimatorSet animator) { + if (animator != null) { + animator.removeAllListeners(); + animator.cancel(); + } + return null; } } diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index 57aec32801..d59c64431e 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -45,6 +45,23 @@ class HotseatIconKeyEventListener implements View.OnKeyListener { } } +/** + * A keyboard listener we set on full screen pages (e.g. custom content). + */ +class FullscreenKeyEventListener implements View.OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_PAGE_UP) { + // Handle the key event just like a workspace icon would in these cases. In this case, + // it will basically act as if there is a single icon in the top left (so you could + // think of the fullscreen page as a focusable fullscreen widget). + return FocusHelper.handleIconKeyEvent(v, keyCode, event); + } + return false; + } +} + public class FocusHelper { private static final String TAG = "FocusHelper"; @@ -72,7 +89,6 @@ public class FocusHelper { KeyEvent.keyCodeToString(keyCode))); } - if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { if (LauncherAppState.isDogfoodBuild()) { throw new IllegalStateException("Parent of the focused item is not supported."); @@ -84,8 +100,6 @@ public class FocusHelper { // Initialize variables. final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout cellLayout = (CellLayout) itemContainer.getParent(); - final int countX = cellLayout.getCountX(); - final int countY = cellLayout.getCountY(); final int iconIndex = itemContainer.indexOfChild(v); final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent(); @@ -96,8 +110,8 @@ public class FocusHelper { int[][] matrix = FocusLogic.createSparseMatrix(cellLayout); // Process focus. - int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, - countY, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl); + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, + pageCount, isLayoutRtl); if (newIconIndex == FocusLogic.NOOP) { handleNoopKey(keyCode, v); return consume; @@ -114,7 +128,8 @@ public class FocusHelper { pagedView.snapToPage(pageIndex - 1); child = newParent.getChildAt( ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) - ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row); + ^ newParent.invertLayoutHorizontally()) ? 0 : matrix.length - 1, + row); } break; case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: @@ -128,7 +143,7 @@ public class FocusHelper { newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); if (newParent != null) { pagedView.snapToPage(pageIndex - 1); - child = newParent.getChildAt(countX - 1, countY - 1); + child = newParent.getChildAt(matrix.length - 1, matrix[0].length - 1); } break; case FocusLogic.NEXT_PAGE_FIRST_ITEM: @@ -143,7 +158,8 @@ public class FocusHelper { newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); if (newParent != null) { pagedView.snapToPage(pageIndex + 1); - child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex); + child = FocusLogic.getAdjacentChildInNextFolderPage( + newParent, v, newIconIndex); } break; case FocusLogic.CURRENT_PAGE_FIRST_ITEM: @@ -174,7 +190,7 @@ public class FocusHelper { } /** - * Handles key events in the workspace hot seat (bottom of the screen). + * Handles key events in the workspace hotseat (bottom of the screen). *

Currently we don't special case for the phone UI in different orientations, even though * the hotseat is on the side in landscape mode. This is to ensure that accessibility * consistency is maintained across rotations. @@ -185,7 +201,8 @@ public class FocusHelper { return consume; } - DeviceProfile profile = ((Launcher) v.getContext()).getDeviceProfile(); + final Launcher launcher = (Launcher) v.getContext(); + final DeviceProfile profile = launcher.getDeviceProfile(); if (DEBUG) { Log.v(TAG, String.format( @@ -194,15 +211,13 @@ public class FocusHelper { } // Initialize the variables. + final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); - Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); - Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + final ItemInfo itemInfo = (ItemInfo) v.getTag(); int pageIndex = workspace.getNextPage(); int pageCount = workspace.getChildCount(); - int countX = -1; - int countY = -1; int iconIndex = hotseatParent.indexOfChild(v); int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() .getChildAt(iconIndex).getLayoutParams()).cellX; @@ -221,51 +236,91 @@ public class FocusHelper { if (keyCode == KeyEvent.KEYCODE_DPAD_UP && !profile.isVerticalBarLayout()) { - matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, - true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, - iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, + true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank); iconIndex += iconParent.getChildCount(); - countX = iconLayout.getCountX(); - countY = iconLayout.getCountY() + hotseatLayout.getCountY(); parent = iconParent; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && profile.isVerticalBarLayout()) { - matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, - false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, - iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, + false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank); iconIndex += iconParent.getChildCount(); - countX = iconLayout.getCountX() + hotseatLayout.getCountX(); - countY = iconLayout.getCountY(); parent = iconParent; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && profile.isVerticalBarLayout()) { keyCode = KeyEvent.KEYCODE_PAGE_DOWN; - }else { + } else if (isUninstallKeyChord(e)) { + matrix = FocusLogic.createSparseMatrix(iconLayout); + if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) { + UninstallDropTarget.startUninstallActivity(launcher, itemInfo); + } + } else if (isDeleteKeyChord(e)) { + matrix = FocusLogic.createSparseMatrix(iconLayout); + launcher.removeItem(v, itemInfo, true /* deleteFromDb */); + } else { // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the // matrix extended with hotseat. matrix = FocusLogic.createSparseMatrix(hotseatLayout); - countX = hotseatLayout.getCountX(); - countY = hotseatLayout.getCountY(); parent = hotseatParent; } // Process the focus. - int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, - countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, + pageCount, Utilities.isRtl(v.getResources())); View newIcon = null; - if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - newIcon = parent.getChildAt(0); - // TODO(hyunyoungs): handle cases where the child is not an icon but - // a folder or a widget. - workspace.snapToPage(pageIndex + 1); + switch (newIconIndex) { + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex + 1); + break; + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(0); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex - 1); + break; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(parent.getChildCount() - 1); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex - 1); + break; + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + // Go to the previous page but keep the focus on the same hotseat icon. + workspace.snapToPage(pageIndex - 1); + // If the page we are going to is fullscreen, have it take the focus from hotseat. + CellLayout prevPage = (CellLayout) workspace.getPageAt(pageIndex - 1); + boolean isPrevPageFullscreen = ((CellLayout.LayoutParams) prevPage + .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen; + if (isPrevPageFullscreen) { + workspace.getPageAt(pageIndex - 1).requestFocus(); + } + break; + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + // Go to the next page but keep the focus on the same hotseat icon. + workspace.snapToPage(pageIndex + 1); + // If the page we are going to is fullscreen, have it take the focus from hotseat. + CellLayout nextPage = (CellLayout) workspace.getPageAt(pageIndex + 1); + boolean isNextPageFullscreen = ((CellLayout.LayoutParams) nextPage + .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen; + if (isNextPageFullscreen) { + workspace.getPageAt(pageIndex + 1).requestFocus(); + } + break; } if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { newIconIndex -= iconParent.getChildCount(); } if (parent != null) { - if (newIcon == null && newIconIndex >=0) { + if (newIcon == null && newIconIndex >= 0) { newIcon = parent.getChildAt(newIconIndex); } if (newIcon != null) { @@ -301,11 +356,10 @@ public class FocusHelper { final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar); final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat); + final ItemInfo itemInfo = (ItemInfo) v.getTag(); final int iconIndex = parent.indexOfChild(v); final int pageIndex = workspace.indexOfChild(iconLayout); final int pageCount = workspace.getChildCount(); - int countX = iconLayout.getCountX(); - int countY = iconLayout.getCountY(); CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); @@ -315,27 +369,30 @@ public class FocusHelper { // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended // with the hotseat. if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { - matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */, - profile.inv.hotseatAllAppsRank, - !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); - countY = countY + 1; + matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, + true /* horizontal */, profile.inv.hotseatAllAppsRank); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && profile.isVerticalBarLayout()) { - matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */, - profile.inv.hotseatAllAppsRank, - !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); - countX = countX + 1; - } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { - workspace.removeWorkspaceItem(v); - return consume; + matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, + false /* horizontal */, profile.inv.hotseatAllAppsRank); + } else if (isUninstallKeyChord(e)) { + matrix = FocusLogic.createSparseMatrix(iconLayout); + if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) { + UninstallDropTarget.startUninstallActivity(launcher, itemInfo); + } + } else if (isDeleteKeyChord(e)) { + matrix = FocusLogic.createSparseMatrix(iconLayout); + launcher.removeItem(v, itemInfo, true /* deleteFromDb */); } else { matrix = FocusLogic.createSparseMatrix(iconLayout); } // Process the focus. - int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, - countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, + pageCount, Utilities.isRtl(v.getResources())); + boolean isRtl = Utilities.isRtl(v.getResources()); View newIcon = null; + CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex); switch (newIconIndex) { case FocusLogic.NOOP: if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { @@ -350,32 +407,37 @@ public class FocusHelper { } int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); - workspace.snapToPage(newPageIndex); if (parent != null) { - workspace.snapToPage(newPageIndex); iconLayout = (CellLayout) parent.getParent(); - matrix = FocusLogic.createSparseMatrix(iconLayout, - iconLayout.getCountX(), row); - newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, - matrix, FocusLogic.PIVOT, newPageIndex, pageCount, - Utilities.isRtl(v.getResources())); - newIcon = parent.getChildAt(newIconIndex); + matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, + iconLayout.getCountX(), row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT, + newPageIndex, pageCount, Utilities.isRtl(v.getResources())); + if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { + newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, + isRtl); + } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) { + newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, + isRtl); + } else { + newIcon = parent.getChildAt(newIconIndex); + } } break; case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - newIcon = parent.getChildAt(0); - workspace.snapToPage(pageIndex - 1); + workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1); + newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); + if (newIcon == null) { + // Check the hotseat if no focusable item was found on the workspace. + newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); + workspace.snapToPage(pageIndex - 1); + } break; case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - newIcon = parent.getChildAt(parent.getChildCount() - 1); - workspace.snapToPage(pageIndex - 1); + newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl); break; case FocusLogic.NEXT_PAGE_FIRST_ITEM: - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - newIcon = parent.getChildAt(0); - workspace.snapToPage(pageIndex + 1); + newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl); break; case FocusLogic.NEXT_PAGE_LEFT_COLUMN: case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: @@ -383,24 +445,37 @@ public class FocusHelper { if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) { newPageIndex = pageIndex - 1; } - workspace.snapToPage(newPageIndex); row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); if (parent != null) { - workspace.snapToPage(newPageIndex); iconLayout = (CellLayout) parent.getParent(); - matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); - newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, - matrix, FocusLogic.PIVOT, newPageIndex, pageCount, - Utilities.isRtl(v.getResources())); - newIcon = parent.getChildAt(newIconIndex); + matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT, + newPageIndex, pageCount, Utilities.isRtl(v.getResources())); + if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { + newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, + isRtl); + } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) { + newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, + isRtl); + } else { + newIcon = parent.getChildAt(newIconIndex); + } } break; case FocusLogic.CURRENT_PAGE_FIRST_ITEM: - newIcon = parent.getChildAt(0); + newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); + if (newIcon == null) { + // Check the hotseat if no focusable item was found on the workspace. + newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); + } break; case FocusLogic.CURRENT_PAGE_LAST_ITEM: - newIcon = parent.getChildAt(parent.getChildCount() - 1); + newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl); + if (newIcon == null) { + // Check the hotseat if no focusable item was found on the workspace. + newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl); + } break; default: // current page, some item. @@ -457,4 +532,81 @@ public class FocusHelper { break; } } + + /** + * Returns whether the key event represents a valid uninstall key chord. + */ + private static boolean isUninstallKeyChord(KeyEvent event) { + int keyCode = event.getKeyCode(); + return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) && + event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON); + } + + /** + * Returns whether the key event represents a valid delete key chord. + */ + private static boolean isDeleteKeyChord(KeyEvent event) { + int keyCode = event.getKeyCode(); + return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) && + event.hasModifiers(KeyEvent.META_CTRL_ON); + } + + private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout, + int pageIndex, boolean isRtl) { + if (pageIndex - 1 < 0) { + return null; + } + CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1); + View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl); + if (newIcon == null) { + // Check the hotseat if no focusable item was found on the workspace. + newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl); + workspace.snapToPage(pageIndex - 1); + } + return newIcon; + } + + private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout, + int pageIndex, boolean isRtl) { + if (pageIndex + 1 >= workspace.getPageCount()) { + return null; + } + CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1); + View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); + if (newIcon == null) { + // Check the hotseat if no focusable item was found on the workspace. + newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); + workspace.snapToPage(pageIndex + 1); + } + return newIcon; + } + + private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) { + View icon; + int countX = cellLayout.getCountX(); + for (int y = 0; y < cellLayout.getCountY(); y++) { + int increment = isRtl ? -1 : 1; + for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) { + if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) { + return icon; + } + } + } + return null; + } + + private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout, + boolean isRtl) { + View icon; + int countX = cellLayout.getCountX(); + for (int y = cellLayout.getCountY() - 1; y >= 0; y--) { + int increment = isRtl ? 1 : -1; + for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) { + if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) { + return icon; + } + } + } + return null; + } } diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java index ecf93e4b3d..58b38ebcd2 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -30,6 +30,7 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen // It can be any number >0. The view is resized using scaleX and scaleY. static final int DEFAULT_LAYOUT_SIZE = 100; + private static final float MIN_VISIBLE_ALPHA = 0.2f; private static final long ANIM_DURATION = 150; @@ -41,6 +42,7 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen private View mLastFocusedView; private boolean mInitiated; + private final OnFocusChangeListener mHideIndicatorOnFocusListener; private Pair mPendingCall; @@ -52,6 +54,16 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen super(context, attrs); setAlpha(0); setBackgroundColor(getResources().getColor(R.color.focused_background)); + + mHideIndicatorOnFocusListener = new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + endCurrentAnimation(); + setAlpha(0); + } + } + }; } @Override @@ -66,6 +78,13 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } } + /** + * Sets the alpha of this FocusIndicatorView to 0 when a view with this listener receives focus. + */ + public View.OnFocusChangeListener getHideIndicatorOnFocusListener() { + return mHideIndicatorOnFocusListener; + } + @Override public void onFocusChange(View v, boolean hasFocus) { mPendingCall = null; diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index c1aa35669b..471c324fe4 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -57,6 +57,7 @@ import com.android.launcher3.FolderInfo.FolderListener; import com.android.launcher3.UninstallDropTarget.UninstallSource; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.UiThreadCircularReveal; @@ -318,9 +319,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, String.format(getContext().getString(R.string.folder_renamed), newTitle)); } - // In order to clear the focus from the text field, we set the focus on ourself. This - // ensures that every time the field is clicked, focus is gained, giving reliable behavior. - requestFocus(); + + // This ensures that focus is gained every time the field is clicked, which selects all + // the text and brings up the soft keyboard if necessary. + mFolderName.clearFocus(); Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); mIsEditingName = false; @@ -414,13 +416,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList /** * Creates a new UserFolder, inflated from R.layout.user_folder. * - * @param context The application's context. + * @param launcher The main activity. * * @return A new UserFolder. */ @SuppressLint("InflateParams") static Folder fromXml(Launcher launcher) { - return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, null); + return (Folder) launcher.getLayoutInflater().inflate( + FeatureFlags.LAUNCHER3_ICON_NORMALIZATION + ? R.layout.user_folder_icon_normalized : R.layout.user_folder, null); } /** @@ -451,6 +455,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mContent.snapToPageImmediately(0); } + // This is set to true in close(), but isn't reset to false until onDropCompleted(). This + // leads to an consistent state if you drag out of the folder and drag back in without + // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. + mDeleteFolderOnDropCompleted = false; + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.ATLEAST_LOLLIPOP) { @@ -541,10 +550,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onAnimationEnd(Animator animation) { mState = STATE_OPEN; - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - + onCompleteRunnable.run(); mContent.setFocusOnFirstChild(); } }); @@ -639,9 +645,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList oa.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - onCloseComplete(); setLayerType(LAYER_TYPE_NONE, null); - mState = STATE_SMALL; + close(true); } @Override public void onAnimationStart(Animator animation) { @@ -655,6 +660,34 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList oa.start(); } + public void close(boolean wasAnimated) { + // TODO: Clear all active animations. + DragLayer parent = (DragLayer) getParent(); + if (parent != null) { + parent.removeView(this); + } + mDragController.removeDropTarget(this); + clearFocus(); + if (wasAnimated) { + mFolderIcon.requestFocus(); + } + + if (mRearrangeOnClose) { + rearrangeChildren(); + mRearrangeOnClose = false; + } + if (getItemCount() <= 1) { + if (!mDragInProgress && !mSuppressFolderDeletion) { + replaceFolderWithFinalItem(); + } else if (mDragInProgress) { + mDeleteFolderOnDropCompleted = true; + } + } + mSuppressFolderDeletion = false; + clearDragInfo(); + mState = STATE_SMALL; + } + public boolean acceptDrop(DragObject d) { final ItemInfo item = (ItemInfo) d.dragInfo; final int itemType = item.itemType; @@ -706,6 +739,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); mReorderAlarm.setAlarm(REORDER_DELAY); mPrevTargetRank = mTargetRank; + + if (d.stateAnnouncer != null) { + d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position, + mTargetRank + 1)); + } } float x = r[0]; @@ -867,7 +905,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Show the animation, next time something is added to the folder. mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher); } - } @Override @@ -1082,63 +1119,42 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mContent.getItemCount(); } - @Thunk void onCloseComplete() { - DragLayer parent = (DragLayer) getParent(); - if (parent != null) { - parent.removeView(this); - } - mDragController.removeDropTarget((DropTarget) this); - clearFocus(); - mFolderIcon.requestFocus(); - - if (mRearrangeOnClose) { - rearrangeChildren(); - mRearrangeOnClose = false; - } - if (getItemCount() <= 1) { - if (!mDragInProgress && !mSuppressFolderDeletion) { - replaceFolderWithFinalItem(); - } else if (mDragInProgress) { - mDeleteFolderOnDropCompleted = true; - } - } - mSuppressFolderDeletion = false; - clearDragInfo(); - } - @Thunk void replaceFolderWithFinalItem() { // Add the last remaining child to the workspace in place of the folder Runnable onCompleteRunnable = new Runnable() { @Override public void run() { - CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId); + int itemCount = mInfo.contents.size(); + if (itemCount <= 1) { + View newIcon = null; - View child = null; - // Move the item from the folder to the workspace, in the position of the folder - if (getItemCount() == 1) { - ShortcutInfo finalItem = mInfo.contents.get(0); - child = mLauncher.createShortcut(cellLayout, finalItem); - LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, - mInfo.screenId, mInfo.cellX, mInfo.cellY); - } - if (getItemCount() <= 1) { - // Remove the folder - LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); - if (cellLayout != null) { - // b/12446428 -- sometimes the cell layout has already gone away? - cellLayout.removeView(mFolderIcon); + if (itemCount == 1) { + // Move the item from the folder to the workspace, in the position of the + // folder + CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, + mInfo.screenId); + ShortcutInfo finalItem = mInfo.contents.remove(0); + newIcon = mLauncher.createShortcut(cellLayout, finalItem); + LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, + mInfo.screenId, mInfo.cellX, mInfo.cellY); } + + // Remove the folder + mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */); if (mFolderIcon instanceof DropTarget) { mDragController.removeDropTarget((DropTarget) mFolderIcon); } - mLauncher.removeFolder(mInfo); - } - // We add the child after removing the folder to prevent both from existing at - // the same time in the CellLayout. We need to add the new item with addInScreenFromBind() - // to ensure that hotseat items are placed correctly. - if (child != null) { - mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId, - mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); + + if (newIcon != null) { + // We add the child after removing the folder to prevent both from existing + // at the same time in the CellLayout. We need to add the new item with + // addInScreenFromBind() to ensure that hotseat items are placed correctly. + mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo.container, + mInfo.screenId, mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); + + // Focus the newly created child + newIcon.requestFocus(); + } } } }; @@ -1155,15 +1171,37 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mDestroyed; } - // This method keeps track of the last item in the folder for the purposes + // This method keeps track of the first and last item in the folder for the purposes // of keyboard focus public void updateTextViewFocus() { - View lastChild = mContent.getLastItem(); - if (lastChild != null) { + final View firstChild = mContent.getFirstItem(); + final View lastChild = mContent.getLastItem(); + if (firstChild != null && lastChild != null) { mFolderName.setNextFocusDownId(lastChild.getId()); mFolderName.setNextFocusRightId(lastChild.getId()); mFolderName.setNextFocusLeftId(lastChild.getId()); mFolderName.setNextFocusUpId(lastChild.getId()); + // Hitting TAB from the folder name wraps around to the first item on the current + // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name. + mFolderName.setNextFocusForwardId(firstChild.getId()); + // When clicking off the folder when editing the name, this Folder gains focus. When + // pressing an arrow key from that state, give the focus to the first item. + this.setNextFocusDownId(firstChild.getId()); + this.setNextFocusRightId(firstChild.getId()); + this.setNextFocusLeftId(firstChild.getId()); + this.setNextFocusUpId(firstChild.getId()); + // When pressing shift+tab in the above state, give the focus to the last item. + setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + boolean isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB && + event.hasModifiers(KeyEvent.META_SHIFT_ON); + if (isShiftPlusTab && Folder.this.isFocused()) { + return lastChild.requestFocus(); + } + return false; + } + }); } } @@ -1284,7 +1322,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList rearrangeChildren(); } if (getItemCount() <= 1) { - replaceFolderWithFinalItem(); + if (mInfo.opened) { + mLauncher.closeFolder(this, true); + } else { + replaceFolderWithFinalItem(); + } } } @@ -1326,8 +1368,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } public void onFocusChange(View v, boolean hasFocus) { - if (v == mFolderName && hasFocus) { - startEditingFolderName(); + if (v == mFolderName) { + if (hasFocus) { + startEditingFolderName(); + } else { + dismissEditingName(); + } } } @@ -1339,7 +1385,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } @Override - public void fillInLaunchSourceData(Bundle sourceData) { + public void fillInLaunchSourceData(View v, Bundle sourceData) { // Fill in from the folder icon's launch source provider first Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData); sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER); diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index 8d534d2fef..d7b55b3a2f 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -520,7 +520,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } class PreviewItemDrawingParams { - PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { + PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) { this.transX = transX; this.transY = transY; this.scale = scale; @@ -529,7 +529,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float transX; float transY; float scale; - int overlayAlpha; + float overlayAlpha; Drawable drawable; } @@ -561,7 +561,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop(); float transX = (mAvailableSpaceInPreview - scaledSize) / 2; float totalScale = mBaselineIconScale * scale; - final int overlayAlpha = (int) (80 * (1 - r)); + final float overlayAlpha = (80 * (1 - r)) / 255f; if (params == null) { params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); @@ -585,12 +585,12 @@ public class FolderIcon extends FrameLayout implements FolderListener { d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); if (d instanceof FastBitmapDrawable) { FastBitmapDrawable fd = (FastBitmapDrawable) d; - int oldBrightness = fd.getBrightness(); + float oldBrightness = fd.getBrightness(); fd.setBrightness(params.overlayAlpha); d.draw(canvas); fd.setBrightness(oldBrightness); } else { - d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255), + d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255), PorterDuff.Mode.SRC_ATOP); d.draw(canvas); d.clearColorFilter(); diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index cc9c5738aa..d503d2cf53 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -402,16 +402,28 @@ public class FolderPagedView extends PagedView { return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage; } + public View getFirstItem() { + if (getChildCount() < 1) { + return null; + } + ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets(); + if (mGridCountX > 0) { + return currContainer.getChildAt(0, 0); + } else { + return currContainer.getChildAt(0); + } + } + public View getLastItem() { if (getChildCount() < 1) { return null; } - ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets(); - int lastRank = lastContainer.getChildCount() - 1; + ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets(); + int lastRank = currContainer.getChildCount() - 1; if (mGridCountX > 0) { - return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); + return currContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); } else { - return lastContainer.getChildAt(lastRank); + return currContainer.getChildAt(lastRank); } } diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java index 5ff85d664c..0d68e33b49 100644 --- a/src/com/android/launcher3/HolographicOutlineHelper.java +++ b/src/com/android/launcher3/HolographicOutlineHelper.java @@ -164,6 +164,9 @@ public class HolographicOutlineHelper { int bitmapWidth = (int) (rect.width() * view.getScaleX()); int bitmapHeight = (int) (rect.height() * view.getScaleY()); + if (bitmapHeight <= 0 || bitmapWidth <= 0) { + return null; + } int key = (bitmapWidth << 16) | bitmapHeight; Bitmap cache = mBitmapCache.get(key); diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index 17fdeb1dc8..3e838760ee 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -17,13 +17,13 @@ package com.android.launcher3; import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; @@ -70,7 +70,7 @@ public class Hotseat extends FrameLayout public void setOnLongClickListener(OnLongClickListener l) { mContent.setOnLongClickListener(l); } - + /* Get the orientation invariant order of the item in the hotseat for persistence. */ int getOrderInHotseat(int x, int y) { return mHasVerticalHotseat ? (mContent.getCountY() - y - 1) : x; @@ -118,6 +118,10 @@ public class Hotseat extends FrameLayout Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); mLauncher.resizeIconDrawable(d); + int scaleDownPx = getResources().getDimensionPixelSize(R.dimen.all_apps_button_scale_down); + Rect bounds = d.getBounds(); + d.setBounds(bounds.left, bounds.top + scaleDownPx / 2, bounds.right - scaleDownPx, + bounds.bottom - scaleDownPx / 2); allAppsButton.setCompoundDrawables(null, d, null, null); allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); @@ -150,7 +154,7 @@ public class Hotseat extends FrameLayout } @Override - public void fillInLaunchSourceData(Bundle sourceData) { + public void fillInLaunchSourceData(View v, Bundle sourceData) { sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOTSEAT); } } diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 59ab8397d9..c72c2f9f6d 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -28,7 +28,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -45,8 +45,10 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.PackageItemInfo; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.SQLiteCacheHelper; import com.android.launcher3.util.Thunk; import java.util.Collections; @@ -116,7 +118,7 @@ public class IconCache { mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); mIconDpi = inv.fillResIconDpi; - mIconDb = new IconDB(context); + mIconDb = new IconDB(context, inv.iconBitmapSize); mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); @@ -179,15 +181,7 @@ public class IconCache { private Bitmap makeDefaultIcon(UserHandleCompat user) { Drawable unbadged = getFullResDefaultActivityIcon(); - Drawable d = mUserManager.getBadgedDrawableForUser(unbadged, user); - Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), - Math.max(d.getIntrinsicHeight(), 1), - Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - d.setBounds(0, 0, b.getWidth(), b.getHeight()); - d.draw(c); - c.setBitmap(null); - return b; + return Utilities.createBadgedIconBitmap(unbadged, user, mContext); } /** @@ -237,9 +231,9 @@ public class IconCache { public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { removeFromMemCacheLocked(packageName, user); long userSerial = mUserManager.getSerialNumberForUser(user); - mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + mIconDb.delete( IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", - new String[] {packageName + "/%", Long.toString(userSerial)}); + new String[]{packageName + "/%", Long.toString(userSerial)}); } public void updateDbIcons(Set ignorePackagesForMainUser) { @@ -282,58 +276,65 @@ public class IconCache { componentMap.put(app.getComponentName(), app); } - Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, - new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, - IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, - IconDB.COLUMN_SYSTEM_STATE}, - IconDB.COLUMN_USER + " = ? ", - new String[] {Long.toString(userSerial)}, - null, null, null); - - final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); - final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); - final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); - final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); - final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); - HashSet itemsToRemove = new HashSet(); Stack appsToUpdate = new Stack<>(); - while (c.moveToNext()) { - String cn = c.getString(indexComponent); - ComponentName component = ComponentName.unflattenFromString(cn); - PackageInfo info = pkgInfoMap.get(component.getPackageName()); - if (info == null) { - if (!ignorePackages.contains(component.getPackageName())) { + Cursor c = null; + try { + c = mIconDb.query( + new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, + IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, + IconDB.COLUMN_SYSTEM_STATE}, + IconDB.COLUMN_USER + " = ? ", + new String[]{Long.toString(userSerial)}); + + final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); + final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); + final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); + final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); + final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = pkgInfoMap.get(component.getPackageName()); + if (info == null) { + if (!ignorePackages.contains(component.getPackageName())) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } + continue; + } + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { + // Application is not present + continue; + } + + long updateTime = c.getLong(indexLastUpdate); + int version = c.getInt(indexVersion); + LauncherActivityInfoCompat app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime && + TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { + continue; + } + if (app == null) { remove(component, user); itemsToRemove.add(c.getInt(rowIndex)); + } else { + appsToUpdate.add(app); } - continue; } - if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { - // Application is not present - continue; - } - - long updateTime = c.getLong(indexLastUpdate); - int version = c.getInt(indexVersion); - LauncherActivityInfoCompat app = componentMap.remove(component); - if (version == info.versionCode && updateTime == info.lastUpdateTime && - TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { - continue; - } - if (app == null) { - remove(component, user); - itemsToRemove.add(c.getInt(rowIndex)); - } else { - appsToUpdate.add(app); + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); + // Continue updating whatever we have read so far + } finally { + if (c != null) { + c.close(); } } - c.close(); if (!itemsToRemove.isEmpty()) { - mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, - Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), - null); + mIconDb.delete( + Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), null); } // Insert remaining apps. @@ -363,8 +364,7 @@ public class IconCache { values.put(IconDB.COLUMN_USER, userSerial); values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); values.put(IconDB.COLUMN_VERSION, info.versionCode); - mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, - SQLiteDatabase.CONFLICT_REPLACE); + mIconDb.insertOrReplace(values); } @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, @@ -380,7 +380,8 @@ public class IconCache { } if (entry == null) { entry = new CacheEntry(); - entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); + entry.icon = Utilities.createBadgedIconBitmap( + app.getIcon(mIconDpi), app.getUser(), mContext); } entry.title = app.getLabel(); entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); @@ -542,7 +543,8 @@ public class IconCache { // Check the DB first. if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { if (info != null) { - entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); + entry.icon = Utilities.createBadgedIconBitmap( + info.getIcon(mIconDpi), info.getUser(), mContext); } else { if (usePackageIcon) { CacheEntry packageEntry = getEntryForPackageLocked( @@ -623,9 +625,8 @@ public class IconCache { if (appInfo == null) { throw new NameNotFoundException("ApplicationInfo is null"); } - Drawable drawable = mUserManager.getBadgedDrawableForUser( - appInfo.loadIcon(mPackageManager), user); - entry.icon = Utilities.createIconBitmap(drawable, mContext); + entry.icon = Utilities.createBadgedIconBitmap( + appInfo.loadIcon(mPackageManager), user, mContext); entry.title = appInfo.loadLabel(mPackageManager); entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); entry.isLowResIcon = false; @@ -678,19 +679,18 @@ public class IconCache { label, Color.TRANSPARENT); values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); values.put(IconDB.COLUMN_USER, userSerial); - mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, - SQLiteDatabase.CONFLICT_REPLACE); + mIconDb.insertOrReplace(values); } private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { - Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, - new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, + Cursor c = null; + try { + c = mIconDb.query( + new String[]{lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, IconDB.COLUMN_LABEL}, IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", - new String[] {cacheKey.componentName.flattenToString(), - Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))}, - null, null, null); - try { + new String[]{cacheKey.componentName.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))}); if (c.moveToNext()) { entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); entry.isLowResIcon = lowRes; @@ -704,8 +704,12 @@ public class IconCache { } return true; } + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); } finally { - c.close(); + if (c != null) { + c.close(); + } } return false; } @@ -751,9 +755,9 @@ public class IconCache { LauncherActivityInfoCompat app = mAppsToUpdate.pop(); String cn = app.getComponentName().flattenToString(); ContentValues values = updateCacheAndGetContentValues(app, true); - mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + mIconDb.update(values, IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", - new String[] {cn, Long.toString(mUserSerial)}); + new String[]{cn, Long.toString(mUserSerial)}); mUpdatedPackages.add(app.getComponentName().getPackageName()); if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { @@ -788,9 +792,12 @@ public class IconCache { mSystemState = Locale.getDefault().toString(); } - private static final class IconDB extends SQLiteOpenHelper { + private static final class IconDB extends SQLiteCacheHelper { private final static int DB_VERSION = 7; + private final static int RELEASE_VERSION = DB_VERSION + + (FeatureFlags.LAUNCHER3_ICON_NORMALIZATION ? 1 : 0); + private final static String TABLE_NAME = "icons"; private final static String COLUMN_ROWID = "rowid"; private final static String COLUMN_COMPONENT = "componentName"; @@ -802,12 +809,14 @@ public class IconCache { private final static String COLUMN_LABEL = "label"; private final static String COLUMN_SYSTEM_STATE = "system_state"; - public IconDB(Context context) { - super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); + public IconDB(Context context, int iconPixelSize) { + super(context, LauncherFiles.APP_ICONS_DB, + (RELEASE_VERSION << 16) + iconPixelSize, + TABLE_NAME); } @Override - public void onCreate(SQLiteDatabase db) { + protected void onCreateTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + COLUMN_COMPONENT + " TEXT NOT NULL, " + COLUMN_USER + " INTEGER NOT NULL, " + @@ -820,25 +829,6 @@ public class IconCache { "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + ");"); } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion != newVersion) { - clearDB(db); - } - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion != newVersion) { - clearDB(db); - } - } - - private void clearDB(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } } private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) { diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 3fc8fc0a98..921e90ced8 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -82,18 +82,17 @@ public class InstallShortcutReceiver extends BroadcastReceiver { strings = new HashSet(strings); } strings.add(encoded); - sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).commit(); + sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply(); } } } - public static void removeFromInstallQueue(Context context, ArrayList packageNames, + public static void removeFromInstallQueue(Context context, HashSet packageNames, UserHandleCompat user) { if (packageNames.isEmpty()) { return; } - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences sp = Utilities.getPrefs(context); synchronized(sLock) { Set strings = sp.getStringSet(APPS_PENDING_INSTALL, null); if (DBG) { @@ -111,7 +110,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { newStringsIter.remove(); } } - sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).commit(); + sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).apply(); } } } @@ -132,7 +131,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { infos.add(info); } } - sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet()).commit(); + sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet()).apply(); return infos; } } @@ -145,40 +144,53 @@ public class InstallShortcutReceiver extends BroadcastReceiver { if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { return; } - - PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); - if (info.launchIntent == null || info.label == null) { - if (DBG) Log.e(TAG, "Invalid install shortcut intent"); - return; + PendingInstallShortcutInfo info = createPendingInfo(context, data); + if (info != null) { + queuePendingShortcutInfo(info, context); } - - info = convertToLauncherActivityIfPossible(info); - queuePendingShortcutInfo(info, context); } - public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { + /** + * @return true is the extra is either null or is of type {@param type} + */ + private static boolean isValidExtraType(Intent intent, String key, Class type) { + Object extra = intent.getParcelableExtra(key); + return extra == null || type.isInstance(extra); + } + + /** + * Verifies the intent and creates a {@link PendingInstallShortcutInfo} + */ + private static PendingInstallShortcutInfo createPendingInfo(Context context, Intent data) { + if (!isValidExtraType(data, Intent.EXTRA_SHORTCUT_INTENT, Intent.class) || + !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.class)) || + !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON, Bitmap.class))) { + + if (DBG) Log.e(TAG, "Invalid install shortcut intent"); + return null; + } + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); if (info.launchIntent == null || info.label == null) { if (DBG) Log.e(TAG, "Invalid install shortcut intent"); return null; } - info = convertToLauncherActivityIfPossible(info); - return info.getShortcutInfo(); + + return convertToLauncherActivityIfPossible(info); } - static void queueInstallShortcut(LauncherActivityInfoCompat info, Context context) { - queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context); + public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { + PendingInstallShortcutInfo info = createPendingInfo(context, data); + return info == null ? null : info.getShortcutInfo(); } private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) { // Queue the item up for adding if launcher has not loaded properly yet - LauncherAppState.setApplicationContext(context.getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); boolean launcherNotLoaded = app.getModel().getCallback() == null; - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); - addToInstallQueue(sp, info); + addToInstallQueue(Utilities.getPrefs(context), info); if (!mUseInstallQueue && !launcherNotLoaded) { flushInstallQueue(context); } @@ -192,22 +204,21 @@ public class InstallShortcutReceiver extends BroadcastReceiver { flushInstallQueue(context); } static void flushInstallQueue(Context context) { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences sp = Utilities.getPrefs(context); ArrayList installQueue = getAndClearInstallQueue(sp, context); if (!installQueue.isEmpty()) { Iterator iter = installQueue.iterator(); ArrayList addShortcuts = new ArrayList(); while (iter.hasNext()) { final PendingInstallShortcutInfo pendingInfo = iter.next(); - final Intent intent = pendingInfo.launchIntent; // If the intent specifies a package, make sure the package exists String packageName = pendingInfo.getTargetPackage(); if (!TextUtils.isEmpty(packageName)) { UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) { - if (DBG) Log.d(TAG, "Ignoring shortcut for absent package:" + intent); + if (DBG) Log.d(TAG, "Ignoring shortcut for absent package: " + + pendingInfo.launchIntent); continue; } } @@ -352,7 +363,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { return packageName; } - public boolean isLuncherActivity() { + public boolean isLauncherActivity() { return activityInfo != null; } } @@ -395,9 +406,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return new PendingInstallShortcutInfo(data, context); - } catch (JSONException e) { - Log.d(TAG, "Exception reading shortcut to add: " + e); - } catch (URISyntaxException e) { + } catch (JSONException | URISyntaxException e) { Log.d(TAG, "Exception reading shortcut to add: " + e); } return null; @@ -410,7 +419,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { */ private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( PendingInstallShortcutInfo original) { - if (original.isLuncherActivity()) { + if (original.isLauncherActivity()) { // Already an activity target return original; } diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index ae204c40c3..d60132270a 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -19,7 +19,6 @@ package com.android.launcher3; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Point; -import android.os.Build; import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; @@ -66,25 +65,25 @@ public class InvariantDeviceProfile { */ public int numFolderRows; public int numFolderColumns; - float iconSize; - int iconBitmapSize; - int fillResIconDpi; - float iconTextSize; + public float iconSize; + public int iconBitmapSize; + public int fillResIconDpi; + public float iconTextSize; /** * Number of icons inside the hotseat area. */ - float numHotseatIcons; + public int numHotseatIcons; float hotseatIconSize; int defaultLayoutId; // Derived invariant properties - int hotseatAllAppsRank; + public int hotseatAllAppsRank; DeviceProfile landscapeProfile; DeviceProfile portraitProfile; - InvariantDeviceProfile() { + public InvariantDeviceProfile() { } public InvariantDeviceProfile(InvariantDeviceProfile p) { @@ -95,7 +94,7 @@ public class InvariantDeviceProfile { } InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, - float is, float its, float hs, float his, int dlId) { + float is, float its, int hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) if (hs % 2 == 0) { throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); @@ -116,7 +115,7 @@ public class InvariantDeviceProfile { defaultLayoutId = dlId; } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @TargetApi(23) InvariantDeviceProfile(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); @@ -174,9 +173,9 @@ public class InvariantDeviceProfile { // width, height, #rows, #columns, #folder rows, #folder columns, // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", 275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", @@ -184,9 +183,9 @@ public class InvariantDeviceProfile { predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", - 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); - predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", 359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", + 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", 406, 694, 5, 5, 4, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation @@ -195,9 +194,9 @@ public class InvariantDeviceProfile { 575, 904, 5, 6, 4, 5, 4, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", - 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 76, R.xml.default_workspace_5x6)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_5x6)); return predefinedDeviceProfiles; } @@ -252,8 +251,8 @@ public class InvariantDeviceProfile { ArrayList pointsByNearness = points; Collections.sort(pointsByNearness, new Comparator() { public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { - return (int) (dist(width, height, a.minWidthDps, a.minHeightDps) - - dist(width, height, b.minWidthDps, b.minHeightDps)); + return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps), + dist(width, height, b.minWidthDps, b.minHeightDps)); } }); diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index f7e0ea488e..c7a6807190 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -197,4 +197,11 @@ public class ItemInfo { + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + " user=" + user + ")"; } + + /** + * Whether this item is disabled. + */ + public boolean isDisabled() { + return false; + } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index d6bc07c5f9..ff120f7fd1 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -52,6 +52,7 @@ import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; @@ -72,7 +73,6 @@ import android.text.TextUtils; import android.text.method.TextKeyListener; import android.util.Log; import android.view.Display; -import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -85,13 +85,11 @@ import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewStub; import android.view.ViewTreeObserver; -import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.animation.OvershootInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -99,6 +97,7 @@ import android.widget.Toast; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.PagedView.PageSwitchListener; import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.DefaultAppSearchController; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; @@ -107,6 +106,7 @@ import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.TestingUtils; import com.android.launcher3.util.Thunk; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.WidgetHostViewLoader; @@ -120,11 +120,11 @@ import java.io.PrintWriter; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; /** * Default launcher application. @@ -136,7 +136,7 @@ public class Launcher extends Activity static final boolean LOGD = false; static final boolean PROFILE_STARTUP = false; - static final boolean DEBUG_WIDGETS = true; + static final boolean DEBUG_WIDGETS = false; static final boolean DEBUG_STRICT_MODE = false; static final boolean DEBUG_RESUME_TIME = false; static final boolean DEBUG_DUMP_LOG = false; @@ -195,8 +195,6 @@ public class Launcher extends Activity private static final String RUNTIME_STATE_PENDING_ADD_WIDGET_INFO = "launcher.add_widget_info"; // Type: parcelable private static final String RUNTIME_STATE_PENDING_ADD_WIDGET_ID = "launcher.add_widget_id"; - // Type: int[] - private static final String RUNTIME_STATE_VIEW_IDS = "launcher.view_ids"; static final String INTRO_SCREEN_DISMISSED = "launcher.intro_screen_dismissed"; static final String FIRST_RUN_ACTIVITY_DISPLAYED = "launcher.first_run_activity_displayed"; @@ -205,9 +203,6 @@ public class Launcher extends Activity static final String ACTION_FIRST_LOAD_COMPLETE = "com.android.launcher3.action.FIRST_LOAD_COMPLETE"; - public static final String SHOW_WEIGHT_WATCHER = "debug.show_mem"; - public static final boolean SHOW_WEIGHT_WATCHER_DEFAULT = false; - private static final String QSB_WIDGET_ID = "qsb_widget_id"; private static final String QSB_WIDGET_PROVIDER = "qsb_widget_provider"; @@ -230,9 +225,6 @@ public class Launcher extends Activity private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500; private static final int ACTIVITY_START_DELAY = 1000; - private HashMap mItemIdToViewId = new HashMap(); - private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); - // How long to wait before the new-shortcut animation automatically pans the workspace private static int NEW_APPS_PAGE_MOVE_DELAY = 500; private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5; @@ -248,7 +240,8 @@ public class Launcher extends Activity private View mPageIndicators; @Thunk DragLayer mDragLayer; private DragController mDragController; - private View mWeightWatcher; + + public View mWeightWatcher; private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; @@ -348,6 +341,8 @@ public class Launcher extends Activity private DeviceProfile mDeviceProfile; + private boolean mMoveToDefaultScreenFromNewIntent; + // This is set to the view that launched the activity that navigated the user away from // launcher. Since there is no callback for when the activity has finished launching, enable // the press state and keep this reference to reset the press state when we return to launcher. @@ -356,10 +351,9 @@ public class Launcher extends Activity protected static HashMap sCustomAppWidgets = new HashMap(); - private static final boolean ENABLE_CUSTOM_WIDGET_TEST = false; static { - if (ENABLE_CUSTOM_WIDGET_TEST) { - sCustomAppWidgets.put(DummyWidget.class.getName(), new DummyWidget()); + if (TestingUtils.ENABLE_CUSTOM_WIDGET_TEST) { + TestingUtils.addDummyWidget(sCustomAppWidgets); } } @@ -425,17 +419,15 @@ public class Launcher extends Activity super.onCreate(savedInstanceState); - LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); // Load configuration-specific DeviceProfile mDeviceProfile = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? - app.getInvariantDeviceProfile().landscapeProfile - : app.getInvariantDeviceProfile().portraitProfile; + app.getInvariantDeviceProfile().landscapeProfile + : app.getInvariantDeviceProfile().portraitProfile; - mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), - Context.MODE_PRIVATE); + mSharedPrefs = Utilities.getPrefs(this); mIsSafeModeEnabled = getPackageManager().isSafeMode(); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); @@ -463,6 +455,8 @@ public class Launcher extends Activity setContentView(R.layout.launcher); + app.getInvariantDeviceProfile().landscapeProfile.setSearchBarHeight(getSearchBarHeight()); + app.getInvariantDeviceProfile().portraitProfile.setSearchBarHeight(getSearchBarHeight()); setupViews(); mDeviceProfile.layout(this); @@ -494,11 +488,11 @@ public class Launcher extends Activity IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mCloseSystemDialogsReceiver, filter); - mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext()); + mRotationEnabled = getResources().getBoolean(R.bool.allow_rotation); // In case we are on a device with locked rotation, we should look at preferences to check // if the user has specifically allowed rotation. if (!mRotationEnabled) { - mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext(), false); + mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext()); } // On large interfaces, or on devices that a user has specifically enabled screen rotation, @@ -656,34 +650,12 @@ public class Launcher extends Activity return !isWorkspaceLoading(); } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - public static int generateViewId() { - if (Utilities.ATLEAST_JB_MR1) { - return View.generateViewId(); - } else { - // View.generateViewId() is not available. The following fallback logic is a copy - // of its implementation. - for (;;) { - final int result = sNextGeneratedId.get(); - // aapt-generated IDs have the high byte nonzero; clamp to the range under that. - int newValue = result + 1; - if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. - if (sNextGeneratedId.compareAndSet(result, newValue)) { - return result; - } - } - } - } - public int getViewIdForItem(ItemInfo info) { - // This cast is safe given the > 2B range for int. - int itemId = (int) info.id; - if (mItemIdToViewId.containsKey(itemId)) { - return mItemIdToViewId.get(itemId); - } - int viewId = generateViewId(); - mItemIdToViewId.put(itemId, viewId); - return viewId; + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + // This cast is safe as long as the id < 0x00FFFFFF + // Since we jail all the dynamically generated views, there should be no clashes + // with any other views. + return (int) info.id; } /** @@ -751,6 +723,9 @@ public class Launcher extends Activity return; } else if (requestCode == REQUEST_PICK_WALLPAPER) { if (resultCode == RESULT_OK && mWorkspace.isInOverviewMode()) { + // User could have free-scrolled between pages before picking a wallpaper; make sure + // we move to the closest one now to avoid visual jump. + mWorkspace.setCurrentPage(mWorkspace.getPageNearestToCenterOfScreen()); showWorkspace(false); } return; @@ -927,8 +902,7 @@ public class Launcher extends Activity } @Thunk void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { - CellLayout cellLayout = - (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); + CellLayout cellLayout = mWorkspace.getScreenWithId(mPendingAddInfo.screenId); Runnable onCompleteRunnable = null; int animationType = 0; @@ -965,6 +939,9 @@ public class Launcher extends Activity protected void onStop() { super.onStop(); FirstFrameAnimatorHelper.setIsVisible(false); + if (Utilities.isNycOrAbove()) { + mAppWidgetHost.stopListening(); + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onStop(); @@ -975,6 +952,9 @@ public class Launcher extends Activity protected void onStart() { super.onStart(); FirstFrameAnimatorHelper.setIsVisible(true); + if (Utilities.isNycOrAbove()) { + mAppWidgetHost.startListening(); + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onStart(); @@ -1062,21 +1042,30 @@ public class Launcher extends Activity // Consequently, the widgets will be inflated in the orientation of the foreground activity // (framework issue). On resuming, we ensure that any widgets are inflated for the current // orientation. - getWorkspace().reinflateWidgetsIfNecessary(); + if (!isWorkspaceLoading()) { + getWorkspace().reinflateWidgetsIfNecessary(); + } reinflateQSBIfNecessary(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime)); } - if (mWorkspace.getCustomContentCallbacks() != null) { + // We want to suppress callbacks about CustomContent being shown if we have just received + // onNewIntent while the user was present within launcher. In that case, we post a call + // to move the user to the main screen (which will occur after onResume). We don't want to + // have onHide (from onPause), then onShow, then onHide again, which we get if we don't + // suppress here. + if (mWorkspace.getCustomContentCallbacks() != null + && !mMoveToDefaultScreenFromNewIntent) { // If we are resuming and the custom content is the current page, we call onShow(). - // It is also poassible that onShow will instead be called slightly after first layout + // It is also possible that onShow will instead be called slightly after first layout // if PagedView#setRestorePage was set to the custom content page in onCreate(). if (mWorkspace.isOnOrMovingToCustomContent()) { mWorkspace.getCustomContentCallbacks().onShow(true); } } + mMoveToDefaultScreenFromNewIntent = false; updateInteraction(Workspace.State.NORMAL, mWorkspace.getState()); mWorkspace.onResume(); @@ -1222,7 +1211,7 @@ public class Launcher extends Activity } else { // On devices with a locked orientation, we will at least have the allow rotation // setting. - return !Utilities.isRotationAllowedForDevice(this); + return !getResources().getBoolean(R.bool.allow_rotation); } } @@ -1291,6 +1280,29 @@ public class Launcher extends Activity return handled; } + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { + // Ignore the menu key if we are currently dragging or are on the custom content screen + if (!isOnCustomContent() && !mDragController.isDragging()) { + // Close any open folders + closeFolder(); + + // Stop resizing any widgets + mWorkspace.exitWidgetResizeMode(); + + // Show the overview mode if we are on the workspace + if (mState == State.WORKSPACE && !mWorkspace.isInOverviewMode() && + !mWorkspace.isSwitchingState()) { + mOverviewPanel.requestFocus(); + showOverviewMode(true, true /* requestButtonFocus */); + } + } + return true; + } + return super.onKeyUp(keyCode, event); + } + private String getTypedText() { return mDefaultKeySsb.toString(); } @@ -1322,7 +1334,6 @@ public class Launcher extends Activity * * @param savedState The previous state. */ - @SuppressWarnings("unchecked") private void restoreState(Bundle savedState) { if (savedState == null) { return; @@ -1358,9 +1369,6 @@ public class Launcher extends Activity setWaitingForResult(true); mRestoring = true; } - - mItemIdToViewId = (HashMap) - savedState.getSerializable(RUNTIME_STATE_VIEW_IDS); } /** @@ -1389,45 +1397,8 @@ public class Launcher extends Activity mHotseat.setOnLongClickListener(this); } - mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); - mWidgetsButton = findViewById(R.id.widget_button); - mWidgetsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View arg0) { - if (!mWorkspace.isSwitchingState()) { - onClickAddWidgetButton(arg0); - } - } - }); - mWidgetsButton.setOnTouchListener(getHapticFeedbackTouchListener()); - - View wallpaperButton = findViewById(R.id.wallpaper_button); - wallpaperButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View arg0) { - if (!mWorkspace.isSwitchingState()) { - onClickWallpaperPicker(arg0); - } - } - }); - wallpaperButton.setOnTouchListener(getHapticFeedbackTouchListener()); - - View settingsButton = findViewById(R.id.settings_button); - if (hasSettings()) { - settingsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View arg0) { - if (!mWorkspace.isSwitchingState()) { - onClickSettingsButton(arg0); - } - } - }); - settingsButton.setOnTouchListener(getHapticFeedbackTouchListener()); - } else { - settingsButton.setVisibility(View.GONE); - } - - mOverviewPanel.setAlpha(0f); + // Setup the overview panel + setupOverviewPanel(); // Setup the workspace mWorkspace.setHapticFeedbackEnabled(false); @@ -1445,7 +1416,7 @@ public class Launcher extends Activity if (mLauncherCallbacks != null && mLauncherCallbacks.getAllAppsSearchBarController() != null) { mAppsView.setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController()); } else { - mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); + mAppsView.setSearchBarController(new DefaultAppSearchController()); } // Setup the drag controller (drop targets have to be added in reverse order in priority) @@ -1458,22 +1429,69 @@ public class Launcher extends Activity mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } - if (getResources().getBoolean(R.bool.debug_memory_enabled)) { - Log.v(TAG, "adding WeightWatcher"); - mWeightWatcher = new WeightWatcher(this); - mWeightWatcher.setAlpha(0.5f); - ((FrameLayout) mLauncherView).addView(mWeightWatcher, - new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.BOTTOM) - ); - - boolean show = shouldShowWeightWatcher(); - mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE); + if (TestingUtils.MEMORY_DUMP_ENABLED) { + TestingUtils.addWeightWatcher(this); } } + private void setupOverviewPanel() { + mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); + + // Long-clicking buttons in the overview panel does the same thing as clicking them. + OnLongClickListener performClickOnLongClick = new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return v.performClick(); + } + }; + + // Bind wallpaper button actions + View wallpaperButton = findViewById(R.id.wallpaper_button); + wallpaperButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (!mWorkspace.isSwitchingState()) { + onClickWallpaperPicker(view); + } + } + }); + wallpaperButton.setOnLongClickListener(performClickOnLongClick); + wallpaperButton.setOnTouchListener(getHapticFeedbackTouchListener()); + + // Bind widget button actions + mWidgetsButton = findViewById(R.id.widget_button); + mWidgetsButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (!mWorkspace.isSwitchingState()) { + onClickAddWidgetButton(view); + } + } + }); + mWidgetsButton.setOnLongClickListener(performClickOnLongClick); + mWidgetsButton.setOnTouchListener(getHapticFeedbackTouchListener()); + + // Bind settings actions + View settingsButton = findViewById(R.id.settings_button); + boolean hasSettings = hasSettings(); + if (hasSettings) { + settingsButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (!mWorkspace.isSwitchingState()) { + onClickSettingsButton(view); + } + } + }); + settingsButton.setOnLongClickListener(performClickOnLongClick); + settingsButton.setOnTouchListener(getHapticFeedbackTouchListener()); + } else { + settingsButton.setVisibility(View.GONE); + } + + mOverviewPanel.setAlpha(0f); + } + /** * Sets the all apps button. This method is called from {@link Hotseat}. */ @@ -1582,8 +1600,7 @@ public class Launcher extends Activity ItemInfo info = mPendingAddInfo; if (appWidgetInfo == null) { - appWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(this, - mAppWidgetManager.getAppWidgetInfo(appWidgetId)); + appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId); } if (appWidgetInfo.isCustomWidget) { @@ -1610,18 +1627,28 @@ public class Launcher extends Activity // The AppWidgetHostView has already been inflated and instantiated launcherInfo.hostView = hostView; } - launcherInfo.hostView.setTag(launcherInfo); launcherInfo.hostView.setVisibility(View.VISIBLE); - launcherInfo.notifyWidgetSizeChanged(this); - - mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, info.cellX, - info.cellY, launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); - - addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); + addAppWidgetToWorkspace(launcherInfo, appWidgetInfo, isWorkspaceLocked()); } resetAddInfo(); } + private void addAppWidgetToWorkspace(LauncherAppWidgetInfo item, + LauncherAppWidgetProviderInfo appWidgetInfo, boolean insert) { + item.hostView.setTag(item); + item.onBindAppWidget(this); + + item.hostView.setFocusable(true); + item.hostView.setOnFocusChangeListener(mFocusHandler); + + mWorkspace.addInScreen(item.hostView, item.container, item.screenId, + item.cellX, item.cellY, item.spanX, item.spanY, insert); + + if (!item.isCustomWidget()) { + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + } + } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -1635,7 +1662,10 @@ public class Launcher extends Activity // processing a multi-step drop if (mAppsView != null && mWidgetsView != null && mPendingAddInfo.container == ItemInfo.NO_ID) { - showWorkspace(false); + if (!showWorkspace(false)) { + // If we are already on the workspace, then manually reset all apps + mAppsView.reset(); + } } } else if (Intent.ACTION_USER_PRESENT.equals(action)) { mUserPresent = true; @@ -1668,31 +1698,10 @@ public class Launcher extends Activity } registerReceiver(mReceiver, filter); FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView()); - setupTransparentSystemBarsForLollipop(); mAttached = true; mVisible = true; } - /** - * Sets up transparent navigation and status bars in Lollipop. - * This method is a no-op for other platform versions. - */ - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void setupTransparentSystemBarsForLollipop() { - if (Utilities.ATLEAST_LOLLIPOP) { - Window window = getWindow(); - window.getAttributes().systemUiVisibility |= - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(Color.TRANSPARENT); - window.setNavigationBarColor(Color.TRANSPARENT); - } - } - @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -1730,14 +1739,14 @@ public class Launcher extends Activity mWorkspace.postDelayed(mBuildLayersRunnable, 500); final ViewTreeObserver.OnDrawListener listener = this; mWorkspace.post(new Runnable() { - public void run() { - if (mWorkspace != null && - mWorkspace.getViewTreeObserver() != null) { - mWorkspace.getViewTreeObserver(). - removeOnDrawListener(listener); - } + public void run() { + if (mWorkspace != null && + mWorkspace.getViewTreeObserver() != null) { + mWorkspace.getViewTreeObserver(). + removeOnDrawListener(listener); } - }); + } + }); return; } }); @@ -1795,7 +1804,7 @@ public class Launcher extends Activity } }); - void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { + private void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return; View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId); if (v instanceof Advanceable) { @@ -1805,18 +1814,13 @@ public class Launcher extends Activity } } - void removeWidgetToAutoAdvance(View hostView) { + private void removeWidgetToAutoAdvance(View hostView) { if (mWidgetsToAdvance.containsKey(hostView)) { mWidgetsToAdvance.remove(hostView); updateAutoAdvanceState(); } } - public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { - removeWidgetToAutoAdvance(launcherInfo.hostView); - launcherInfo.hostView = null; - } - public void showOutOfSpaceMessage(boolean isHotseatLayout) { int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); @@ -1898,7 +1902,7 @@ public class Launcher extends Activity // In all these cases, only animate if we're already on home mWorkspace.exitWidgetResizeMode(); - closeFolder(); + closeFolder(alreadyOnHome); exitSpringLoadedDragMode(); // If we are already on home, then just animate back to the workspace, @@ -1911,7 +1915,7 @@ public class Launcher extends Activity final View v = getWindow().peekDecorView(); if (v != null && v.getWindowToken() != null) { - InputMethodManager imm = (InputMethodManager)getSystemService( + InputMethodManager imm = (InputMethodManager) getSystemService( INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } @@ -1943,10 +1947,16 @@ public class Launcher extends Activity mLauncherCallbacks.shouldMoveToDefaultScreenOnHomeIntent() : true; if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() && openFolder == null && moveToDefaultScreen) { + + // We use this flag to suppress noisy callbacks above custom content state + // from onResume. + mMoveToDefaultScreenFromNewIntent = true; mWorkspace.post(new Runnable() { @Override public void run() { - mWorkspace.moveToDefaultScreen(true); + if (mWorkspace != null) { + mWorkspace.moveToDefaultScreen(true); + } } }); } @@ -1976,7 +1986,7 @@ public class Launcher extends Activity outState.putInt(RUNTIME_STATE, mState.ordinal()); // We close any open folder since it will not be re-opened, and we need to make sure // this state is reflected. - closeFolder(); + closeFolder(false); if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screenId > -1 && mWaitingForResult) { @@ -1992,7 +2002,6 @@ public class Launcher extends Activity // Save the current widgets tray? // TODO(hyunyoungs) - outState.putSerializable(RUNTIME_STATE_VIEW_IDS, mItemIdToViewId); if (mLauncherCallbacks != null) { mLauncherCallbacks.onSaveInstanceState(outState); @@ -2162,15 +2171,6 @@ public class Launcher extends Activity } } - public void startSearchFromAllApps(View v, Intent searchIntent, String searchQuery) { - if (mLauncherCallbacks != null && mLauncherCallbacks.startSearchFromAllApps(searchQuery)) { - return; - } - - // If not handled, then just start the provided search intent - startActivitySafely(v, searchIntent, null); - } - public boolean isOnCustomContent() { return mWorkspace.isOnOrMovingToCustomContent(); } @@ -2178,22 +2178,9 @@ public class Launcher extends Activity @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - if (!isOnCustomContent()) { - // Close any open folders - closeFolder(); - // Stop resizing any widgets - mWorkspace.exitWidgetResizeMode(); - if (!mWorkspace.isInOverviewMode()) { - // Show the overview mode - showOverviewMode(true); - } else { - showWorkspace(true); - } - } if (mLauncherCallbacks != null) { return mLauncherCallbacks.onPrepareOptionsMenu(menu); } - return false; } @@ -2243,8 +2230,11 @@ public class Launcher extends Activity mPendingAddInfo.dropPos = null; } - void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, final + void addAppWidgetFromDropImpl(final int appWidgetId, final ItemInfo info, final AppWidgetHostView boundWidget, final LauncherAppWidgetProviderInfo appWidgetInfo) { + if (LOGD) { + Log.d(TAG, "Adding widget from drop"); + } addAppWidgetImpl(appWidgetId, info, boundWidget, appWidgetInfo, 0); } @@ -2352,8 +2342,14 @@ public class Launcher extends Activity AppWidgetHostView hostView = info.boundWidget; int appWidgetId; if (hostView != null) { + // In the case where we've prebound the widget, we remove it from the DragLayer + if (LOGD) { + Log.d(TAG, "Removing widget view from drag layer and setting boundWidget to null"); + } + getDragLayer().removeView(hostView); + appWidgetId = hostView.getAppWidgetId(); - addAppWidgetImpl(appWidgetId, info, hostView, info.info); + addAppWidgetFromDropImpl(appWidgetId, info, hostView, info.info); // Clear the boundWidget so that it doesn't get destroyed. info.boundWidget = null; @@ -2366,7 +2362,7 @@ public class Launcher extends Activity boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( appWidgetId, info.info, options); if (success) { - addAppWidgetImpl(appWidgetId, info, null, info.info); + addAppWidgetFromDropImpl(appWidgetId, info, null, info.info); } else { mPendingAddWidgetInfo = info.info; Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND); @@ -2402,10 +2398,72 @@ public class Launcher extends Activity return newFolder; } - void removeFolder(FolderInfo folder) { + /** + * Unbinds the view for the specified item, and removes the item and all its children. + * + * @param v the view being removed. + * @param itemInfo the {@link ItemInfo} for this view. + * @param deleteFromDb whether or not to delete this item from the db. + */ + public boolean removeItem(View v, ItemInfo itemInfo, boolean deleteFromDb) { + if (itemInfo instanceof ShortcutInfo) { + // Remove the shortcut from the folder before removing it from launcher + FolderInfo folderInfo = sFolders.get(itemInfo.container); + if (folderInfo != null) { + folderInfo.remove((ShortcutInfo) itemInfo); + } else { + mWorkspace.removeWorkspaceItem(v); + } + if (deleteFromDb) { + LauncherModel.deleteItemFromDatabase(this, itemInfo); + } + } else if (itemInfo instanceof FolderInfo) { + final FolderInfo folderInfo = (FolderInfo) itemInfo; + unbindFolder(folderInfo); + mWorkspace.removeWorkspaceItem(v); + if (deleteFromDb) { + LauncherModel.deleteFolderAndContentsFromDatabase(this, folderInfo); + } + } else if (itemInfo instanceof LauncherAppWidgetInfo) { + final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo; + mWorkspace.removeWorkspaceItem(v); + removeWidgetToAutoAdvance(widgetInfo.hostView); + widgetInfo.hostView = null; + if (deleteFromDb) { + deleteWidgetInfo(widgetInfo); + } + + } else { + return false; + } + return true; + } + + /** + * Unbinds any launcher references to the folder. + */ + private void unbindFolder(FolderInfo folder) { sFolders.remove(folder.id); } + /** + * Deletes the widget info and the widget id. + */ + private void deleteWidgetInfo(final LauncherAppWidgetInfo widgetInfo) { + final LauncherAppWidgetHost appWidgetHost = getAppWidgetHost(); + if (appWidgetHost != null && !widgetInfo.isCustomWidget() && widgetInfo.isWidgetIdValid()) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + new AsyncTask() { + public Void doInBackground(Void ... args) { + appWidgetHost.deleteAppWidgetId(widgetInfo.appWidgetId); + return null; + } + }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); + } + LauncherModel.deleteItemFromDatabase(this, widgetInfo); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -2469,6 +2527,9 @@ public class Launcher extends Activity if (mAppWidgetHost != null) { mAppWidgetHost.startListening(); } + + // Recreate the QSB, as the widget has been reset. + bindSearchProviderChanged(); } /** @@ -2535,10 +2596,10 @@ public class Launcher extends Activity final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); if (v.isReadyForClickSetup()) { int widgetId = info.appWidgetId; - AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId); + LauncherAppWidgetProviderInfo appWidgetInfo = + mAppWidgetManager.getLauncherAppWidgetInfo(widgetId); if (appWidgetInfo != null) { - mPendingAddWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo( - this, appWidgetInfo); + mPendingAddWidgetInfo = appWidgetInfo; mPendingAddInfo.copyFrom(info); mPendingAddWidgetId = widgetId; @@ -2620,25 +2681,15 @@ public class Launcher extends Activity final ShortcutInfo shortcut = (ShortcutInfo) tag; if (shortcut.isDisabled != 0) { - int error = R.string.activity_not_available; - if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SAFEMODE) != 0) { - error = R.string.safemode_shortcut_error; - } - Toast.makeText(this, error, Toast.LENGTH_SHORT).show(); - return; - } - - final Intent intent = shortcut.intent; - - // Check for special shortcuts - if (intent.getComponent() != null) { - final String shortcutClass = intent.getComponent().getClassName(); - - if (shortcutClass.equals(MemoryDumpActivity.class.getName())) { - MemoryDumpActivity.startDump(this); - return; - } else if (shortcutClass.equals(ToggleWeightWatcher.class.getName())) { - toggleShowWeightWatcher(); + if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SUSPENDED) != 0 + || (shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_QUIET_USER) != 0) { + // Launch activity anyway, framework will tell the user why the app is suspended. + } else { + int error = R.string.activity_not_available; + if ((shortcut.isDisabled & ShortcutInfo.FLAG_DISABLED_SAFEMODE) != 0) { + error = R.string.safemode_shortcut_error; + } + Toast.makeText(this, error, Toast.LENGTH_SHORT).show(); return; } } @@ -2728,7 +2779,7 @@ public class Launcher extends Activity if (openFolder != null) { folderScreen = mWorkspace.getPageForView(openFolder); // .. and close it - closeFolder(openFolder); + closeFolder(openFolder, true); if (folderScreen != mWorkspace.getCurrentPage()) { // Close any folder open on the current screen closeFolder(); @@ -2765,7 +2816,10 @@ public class Launcher extends Activity */ protected void onClickWallpaperPicker(View v) { if (LOGD) Log.d(TAG, "onClickWallpaperPicker"); - startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()), + int pageScroll = mWorkspace.getScrollForPage(mWorkspace.getPageNearestToCenterOfScreen()); + float offset = mWorkspace.mWallpaperOffset.wallpaperOffsetForScroll(pageScroll); + startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()) + .putExtra(WallpaperPickerActivity.EXTRA_WALLPAPER_OFFSET, offset), REQUEST_PICK_WALLPAPER); if (mLauncherCallbacks != null) { @@ -3063,9 +3117,13 @@ public class Launcher extends Activity } oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); oa.start(); + if (Utilities.isPowerSaverOn(this)) { + // Animations are disabled in battery saver mode, so just skip to the end state. + oa.end(); + } } - private void shrinkAndFadeInFolderIcon(final FolderIcon fi) { + private void shrinkAndFadeInFolderIcon(final FolderIcon fi, boolean animate) { if (fi == null) return; PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); @@ -3091,6 +3149,9 @@ public class Launcher extends Activity } }); oa.start(); + if (!animate) { + oa.end(); + } } /** @@ -3134,30 +3195,38 @@ public class Launcher extends Activity } public void closeFolder() { + closeFolder(true); + } + + public void closeFolder(boolean animate) { Folder folder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; if (folder != null) { if (folder.isEditingName()) { folder.dismissEditingName(); } - closeFolder(folder); + closeFolder(folder, animate); } } - public void closeFolder(Folder folder) { + public void closeFolder(Folder folder, boolean animate) { folder.getInfo().opened = false; ViewGroup parent = (ViewGroup) folder.getParent().getParent(); if (parent != null) { FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); - shrinkAndFadeInFolderIcon(fi); + shrinkAndFadeInFolderIcon(fi, animate); if (fi != null) { ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true; } } - folder.animateClosed(); + if (animate) { + folder.animateClosed(); + } else { + folder.close(false); + } - // Notify the accessibility manager that this folder "window" has disappeard and no - // longer occludeds the workspace items + // Notify the accessibility manager that this folder "window" has disappeared and no + // longer occludes the workspace items getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } @@ -3296,20 +3365,32 @@ public class Launcher extends Activity } } - public void showWorkspace(boolean animated) { - showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null); + /** + * @return whether or not the Launcher state changed. + */ + public boolean showWorkspace(boolean animated) { + return showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null); } - public void showWorkspace(boolean animated, Runnable onCompleteRunnable) { - showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, + /** + * @return whether or not the Launcher state changed. + */ + public boolean showWorkspace(boolean animated, Runnable onCompleteRunnable) { + return showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, onCompleteRunnable); } - protected void showWorkspace(int snapToPage, boolean animated) { - showWorkspace(snapToPage, animated, null); + /** + * @return whether or not the Launcher state changed. + */ + protected boolean showWorkspace(int snapToPage, boolean animated) { + return showWorkspace(snapToPage, animated, null); } - void showWorkspace(int snapToPage, boolean animated, Runnable onCompleteRunnable) { + /** + * @return whether or not the Launcher state changed. + */ + boolean showWorkspace(int snapToPage, boolean animated, Runnable onCompleteRunnable) { boolean changed = mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL; if (changed) { @@ -3335,14 +3416,38 @@ public class Launcher extends Activity getWindow().getDecorView() .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } + return changed; } + /** + * Shows the overview button. + */ void showOverviewMode(boolean animated) { + showOverviewMode(animated, false); + } + + /** + * Shows the overview button, and if {@param requestButtonFocus} is set, will force the focus + * onto one of the overview panel buttons. + */ + void showOverviewMode(boolean animated, boolean requestButtonFocus) { + Runnable postAnimRunnable = null; + if (requestButtonFocus) { + postAnimRunnable = new Runnable() { + @Override + public void run() { + // Hitting the menu button when in touch mode does not trigger touch mode to + // be disabled, so if requested, force focus on one of the overview panel + // buttons. + mOverviewPanel.requestFocusFromTouch(); + } + }; + } mWorkspace.setVisibility(View.VISIBLE); mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), Workspace.State.OVERVIEW, WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, - null /* onCompleteRunnable */); + postAnimRunnable); mState = State.WORKSPACE; } @@ -3493,12 +3598,12 @@ public class Launcher extends Activity // TODO } - protected void disableVoiceButtonProxy(boolean disable) { - // NO-OP + public boolean launcherCallbacksProvidesSearch() { + return (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()); } public View getOrCreateQsbBar() { - if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) { + if (launcherCallbacksProvidesSearch()) { return mLauncherCallbacks.getQsbBar(); } @@ -3512,12 +3617,40 @@ public class Launcher extends Activity opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); - SharedPreferences sp = getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), MODE_PRIVATE); - int widgetId = sp.getInt(QSB_WIDGET_ID, -1); + // Determine the min and max dimensions of the widget. + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile portraitProfile = app.getInvariantDeviceProfile().portraitProfile; + DeviceProfile landscapeProfile = app.getInvariantDeviceProfile().landscapeProfile; + float density = getResources().getDisplayMetrics().density; + Point searchDimens = portraitProfile.getSearchBarDimensForWidgetOpts(getResources()); + int maxHeight = (int) (searchDimens.y / density); + int minHeight = maxHeight; + int maxWidth = (int) (searchDimens.x / density); + int minWidth = maxWidth; + if (!landscapeProfile.isVerticalBarLayout()) { + searchDimens = landscapeProfile.getSearchBarDimensForWidgetOpts(getResources()); + maxHeight = (int) Math.max(maxHeight, searchDimens.y / density); + minHeight = (int) Math.min(minHeight, searchDimens.y / density); + maxWidth = (int) Math.max(maxWidth, searchDimens.x / density); + minWidth = (int) Math.min(minWidth, searchDimens.x / density); + } + opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight); + opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minHeight); + opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxWidth); + opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minWidth); + if (LOGD) { + Log.d(TAG, "QSB widget options: maxHeight=" + maxHeight + " minHeight=" + minHeight + + " maxWidth=" + maxWidth + " minWidth=" + minWidth); + } + + if (mLauncherCallbacks != null) { + opts.putAll(mLauncherCallbacks.getAdditionalSearchWidgetOptions()); + } + + int widgetId = mSharedPrefs.getInt(QSB_WIDGET_ID, -1); AppWidgetProviderInfo widgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId); if (!searchProvider.provider.flattenToString().equals( - sp.getString(QSB_WIDGET_PROVIDER, null)) + mSharedPrefs.getString(QSB_WIDGET_PROVIDER, null)) || (widgetInfo == null) || !widgetInfo.provider.equals(searchProvider.provider)) { // A valid widget is not already bound. @@ -3535,10 +3668,10 @@ public class Launcher extends Activity widgetId = -1; } - sp.edit() + mSharedPrefs.edit() .putInt(QSB_WIDGET_ID, widgetId) .putString(QSB_WIDGET_PROVIDER, searchProvider.provider.flattenToString()) - .commit(); + .apply(); } mAppWidgetHost.setQsbWidgetId(widgetId); @@ -3709,30 +3842,6 @@ public class Launcher extends Activity } } - private boolean shouldShowWeightWatcher() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE); - boolean show = sp.getBoolean(SHOW_WEIGHT_WATCHER, SHOW_WEIGHT_WATCHER_DEFAULT); - - return show; - } - - private void toggleShowWeightWatcher() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE); - boolean show = sp.getBoolean(SHOW_WEIGHT_WATCHER, true); - - show = !show; - - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(SHOW_WEIGHT_WATCHER, show); - editor.commit(); - - if (mWeightWatcher != null) { - mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE); - } - } - public void bindAppsAdded(final ArrayList newScreens, final ArrayList addNotAnimated, final ArrayList addAnimated, @@ -3775,6 +3884,7 @@ public class Launcher extends Activity * * Implementation of the method from LauncherModel.Callbacks. */ + @Override public void bindItems(final ArrayList shortcuts, final int start, final int end, final boolean forceAnimateIcons) { Runnable r = new Runnable() { @@ -3893,6 +4003,16 @@ public class Launcher extends Activity sFolders = folders.clone(); } + private void bindSafeModeWidget(LauncherAppWidgetInfo item) { + PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item, true); + view.updateIcon(mIconCache); + item.hostView = view; + item.hostView.updateAppWidget(null); + item.hostView.setOnClickListener(this); + addAppWidgetToWorkspace(item, null, false); + mWorkspace.requestLayout(); + } + /** * Add the views for a widget to the workspace. * @@ -3908,19 +4028,31 @@ public class Launcher extends Activity return; } + if (mIsSafeModeEnabled) { + bindSafeModeWidget(item); + return; + } + final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0; if (DEBUG_WIDGETS) { Log.d(TAG, "bindAppWidget: " + item); } - final Workspace workspace = mWorkspace; - LauncherAppWidgetProviderInfo appWidgetInfo = - LauncherModel.getProviderInfo(this, item.providerName, item.user); + final LauncherAppWidgetProviderInfo appWidgetInfo; - if (!mIsSafeModeEnabled - && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0) - && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) { + if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) { + // If the provider is not ready, bind as a pending widget. + appWidgetInfo = null; + } 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); + } else { + appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.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) { if (DEBUG_WIDGETS) { Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId @@ -3932,7 +4064,7 @@ public class Launcher extends Activity } // If we do not have a valid id, try to bind an id. - if ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0) { + if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { // Note: This assumes that the id remap broadcast is received before this step. // If that is not the case, the id remap will be ignored and user may see the // click to setup view. @@ -3941,8 +4073,7 @@ public class Launcher extends Activity pendingInfo.spanY = item.spanY; pendingInfo.minSpanX = item.minSpanX; pendingInfo.minSpanY = item.minSpanY; - Bundle options = null; - WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); + Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( @@ -3969,44 +4100,42 @@ public class Launcher extends Activity : LauncherAppWidgetInfo.FLAG_UI_NOT_READY; LauncherModel.updateItemInDatabase(this, item); - } else if (((item.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) + } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) && (appWidgetInfo.configure == null)) { - // If the ID is already valid, verify if we need to configure or not. + // The widget was marked as UI not ready, but there is no configure activity to + // update the UI. item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED; LauncherModel.updateItemInDatabase(this, item); } } - if (!mIsSafeModeEnabled && item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { - final int appWidgetId = item.appWidgetId; + if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { if (DEBUG_WIDGETS) { Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); } - item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + // Verify that we own the widget + if (appWidgetInfo == null) { + Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId); + deleteWidgetInfo(item); + return; + } + + item.hostView = mAppWidgetHost.createView(this, item.appWidgetId, appWidgetInfo); item.minSpanX = appWidgetInfo.minSpanX; item.minSpanY = appWidgetInfo.minSpanY; + addAppWidgetToWorkspace(item, appWidgetInfo, false); } else { - appWidgetInfo = null; PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item, mIsSafeModeEnabled); view.updateIcon(mIconCache); item.hostView = view; item.hostView.updateAppWidget(null); item.hostView.setOnClickListener(this); + addAppWidgetToWorkspace(item, null, false); } - - item.hostView.setTag(item); - item.onBindAppWidget(this); - - workspace.addInScreen(item.hostView, item.container, item.screenId, item.cellX, - item.cellY, item.spanX, item.spanY, false); - if (!item.isCustomWidget()) { - addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); - } - - workspace.requestLayout(); + mWorkspace.requestLayout(); if (DEBUG_WIDGETS) { Log.d(TAG, "bound widget id="+item.appWidgetId+" in " @@ -4128,10 +4257,18 @@ public class Launcher extends Activity return mDeviceProfile.isVerticalBarLayout(); } + /** Returns the search bar bounds in pixels. */ protected Rect getSearchBarBounds() { return mDeviceProfile.getSearchBarBounds(Utilities.isRtl(getResources())); } + public int getSearchBarHeight() { + if (mLauncherCallbacks != null) { + return mLauncherCallbacks.getSearchBarHeight(); + } + return LauncherCallbacks.SEARCH_BAR_HEIGHT_NORMAL; + } + public void bindSearchProviderChanged() { if (mSearchDropTargetBar == null) { return; @@ -4235,7 +4372,7 @@ public class Launcher extends Activity } mWorkspace.removeItemsByComponentName(removedComponents, user); // Notify the drag controller - mDragController.onAppsRemoved(new ArrayList(), removedComponents); + mDragController.onAppsRemoved(new HashSet(), removedComponents); } } @@ -4259,43 +4396,44 @@ public class Launcher extends Activity } /** - * A package was uninstalled. We take both the super set of packageNames + * A package was uninstalled/updated. We take both the super set of packageNames * in addition to specific applications to remove, the reason being that * this can be called when a package is updated as well. In that scenario, - * we only remove specific components from the workspace, where as + * we only remove specific components from the workspace and hotseat, where as * package-removal should clear all items by package name. - * - * @param reason if non-zero, the icons are not permanently removed, rather marked as disabled. - * Implementation of the method from LauncherModel.Callbacks. */ @Override - public void bindComponentsRemoved(final ArrayList packageNames, - final ArrayList appInfos, final UserHandleCompat user, final int reason) { + public void bindWorkspaceComponentsRemoved( + final HashSet packageNames, final HashSet components, + final UserHandleCompat user) { Runnable r = new Runnable() { public void run() { - bindComponentsRemoved(packageNames, appInfos, user, reason); + bindWorkspaceComponentsRemoved(packageNames, components, user); } }; if (waitUntilResume(r)) { return; } + if (!packageNames.isEmpty()) { + mWorkspace.removeItemsByPackageName(packageNames, user); + } + if (!components.isEmpty()) { + mWorkspace.removeItemsByComponentName(components, user); + } + // Notify the drag controller + mDragController.onAppsRemoved(packageNames, components); - if (reason == 0) { - HashSet removedComponents = new HashSet(); - for (AppInfo info : appInfos) { - removedComponents.add(info.componentName); - } - if (!packageNames.isEmpty()) { - mWorkspace.removeItemsByPackageName(packageNames, user); - } - if (!removedComponents.isEmpty()) { - mWorkspace.removeItemsByComponentName(removedComponents, user); - } - // Notify the drag controller - mDragController.onAppsRemoved(packageNames, removedComponents); + } - } else { - mWorkspace.disableShortcutsByPackageName(packageNames, user, reason); + @Override + public void bindAppInfosRemoved(final ArrayList appInfos) { + Runnable r = new Runnable() { + public void run() { + bindAppInfosRemoved(appInfos); + } + }; + if (waitUntilResume(r)) { + return; } // Update AllApps @@ -4304,15 +4442,15 @@ public class Launcher extends Activity } } - private Runnable mBindPackagesUpdatedRunnable = new Runnable() { + private Runnable mBindWidgetModelRunnable = new Runnable() { public void run() { - bindAllPackages(mWidgetsModel); + bindWidgetsModel(mWidgetsModel); } }; @Override - public void bindAllPackages(final WidgetsModel model) { - if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { + public void bindWidgetsModel(WidgetsModel model) { + if (waitUntilResume(mBindWidgetModelRunnable, true)) { mWidgetsModel = model; return; } @@ -4323,6 +4461,13 @@ public class Launcher extends Activity } } + @Override + public void notifyWidgetProvidersChanged() { + if (mWorkspace.getState().shouldUpdateWidget) { + mModel.refreshAndBindWidgetsAndShortcuts(this, mWidgetsView.isEmpty()); + } + } + private int mapConfigurationOriActivityInfoOri(int configOri) { final Display d = getWindowManager().getDefaultDisplay(); int naturalOri = Configuration.ORIENTATION_LANDSCAPE; @@ -4630,8 +4775,9 @@ public class Launcher extends Activity /** * Resizes an icon drawable to the correct icon size. */ - public void resizeIconDrawable(Drawable icon) { + public Drawable resizeIconDrawable(Drawable icon) { icon.setBounds(0, 0, mDeviceProfile.iconSizePx, mDeviceProfile.iconSizePx); + return icon; } /** @@ -4753,7 +4899,15 @@ public class Launcher extends Activity } return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); + } + } + + public static List getFolderContents(View icon) { + if (icon instanceof FolderIcon) { + return ((FolderIcon) icon).getFolder().getItemsInReadingOrder(); + } else { + return Collections.EMPTY_LIST; } } } diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index 6a248a3325..fa20f0360b 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -24,7 +24,6 @@ import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.os.Build; import android.view.View; -import android.view.ViewAnimationUtils; import android.view.ViewTreeObserver; import com.android.launcher3.util.UiThreadCircularReveal; @@ -59,24 +58,25 @@ public class LauncherAnimUtils { // it should be cancelled public static void startAnimationAfterNextDraw(final Animator animator, final View view) { view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { - private boolean mStarted = false; - public void onDraw() { - if (mStarted) return; - mStarted = true; - // Use this as a signal that the animation was cancelled - if (animator.getDuration() == 0) { - return; - } - animator.start(); + private boolean mStarted = false; - final ViewTreeObserver.OnDrawListener listener = this; - view.post(new Runnable() { - public void run() { - view.getViewTreeObserver().removeOnDrawListener(listener); - } - }); + public void onDraw() { + if (mStarted) return; + mStarted = true; + // Use this as a signal that the animation was cancelled + if (animator.getDuration() == 0) { + return; } - }); + animator.start(); + + final ViewTreeObserver.OnDrawListener listener = this; + view.post(new Runnable() { + public void run() { + view.getViewTreeObserver().removeOnDrawListener(listener); + } + }); + } + }); } public static void onDestroyActivity() { diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index d87ad67e5f..e1ade10bab 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -20,13 +20,15 @@ import android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.UserManager; import android.util.Log; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.ConfigMonitor; +import com.android.launcher3.util.TestingUtils; import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; @@ -34,7 +36,6 @@ import java.lang.ref.WeakReference; public class LauncherAppState { private final AppFilter mAppFilter; - private final BuildInfo mBuildInfo; @Thunk final LauncherModel mModel; private final IconCache mIconCache; private final WidgetPreviewLoader mWidgetCache; @@ -79,8 +80,8 @@ public class LauncherAppState { Log.v(Launcher.TAG, "LauncherAppState inited"); - if (sContext.getResources().getBoolean(R.bool.debug_memory_enabled)) { - MemoryTracker.startTrackingMe(sContext, "L"); + if (TestingUtils.MEMORY_DUMP_ENABLED) { + TestingUtils.startTrackingMemory(sContext); } mInvariantDeviceProfile = new InvariantDeviceProfile(sContext); @@ -88,7 +89,6 @@ public class LauncherAppState { mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache); mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class)); - mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class)); mModel = new LauncherModel(this, mIconCache, mAppFilter); LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel); @@ -100,9 +100,15 @@ public class LauncherAppState { // For handling managed profiles filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_AVAILABLE); + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_UNAVAILABLE); sContext.registerReceiver(mModel, filter); UserManagerCompat.getInstance(sContext).enableAndResetCache(); + new ConfigMonitor(sContext).register(); + + sContext.registerReceiver( + new WallpaperChangedReceiver(), new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); } /** @@ -152,14 +158,10 @@ public class LauncherAppState { return sLauncherProvider.get(); } - public static String getSharedPreferencesKey() { - return LauncherFiles.SHARED_PREFERENCES_KEY; - } - public WidgetPreviewLoader getWidgetCache() { return mWidgetCache; } - + public void onWallpaperChanged() { mWallpaperChangedSinceLastCheck = true; } @@ -175,6 +177,6 @@ public class LauncherAppState { } public static boolean isDogfoodBuild() { - return getInstance().mBuildInfo.isDogfoodBuild(); + return FeatureFlags.IS_ALPHA_BUILD || FeatureFlags.IS_DEV_BUILD; } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index 6c3a1e8963..1510558f19 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.os.DeadObjectException; import android.os.TransactionTooLargeException; import android.view.LayoutInflater; import android.view.View; @@ -69,7 +70,8 @@ public class LauncherAppWidgetHost extends AppWidgetHost { try { super.startListening(); } catch (Exception e) { - if (e.getCause() instanceof TransactionTooLargeException) { + if (e.getCause() instanceof TransactionTooLargeException || + e.getCause() instanceof DeadObjectException) { // We're willing to let this slide. The exception is being caused by the list of // RemoteViews which is being passed back. The startListening relationship will // have been established by this point, and we will end up populating the @@ -80,12 +82,6 @@ public class LauncherAppWidgetHost extends AppWidgetHost { } } - @Override - public void stopListening() { - super.stopListening(); - clearViews(); - } - public void addProviderChangeListener(Runnable callback) { mProviderChangeListeners.add(callback); } @@ -100,6 +96,10 @@ public class LauncherAppWidgetHost extends AppWidgetHost { callback.run(); } } + + if (Utilities.ATLEAST_MARSHMALLOW) { + mLauncher.notifyWidgetProvidersChanged(); + } } public AppWidgetHostView createView(Context context, int appWidgetId, @@ -125,5 +125,8 @@ public class LauncherAppWidgetHost extends AppWidgetHost { LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( mLauncher, appWidget); super.onProviderChanged(appWidgetId, info); + // The super method updates the dimensions of the providerInfo. Update the + // launcher spans accordingly. + info.initSpans(); } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java index cf461a5b82..418525758a 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -19,6 +19,8 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.graphics.Rect; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -28,6 +30,8 @@ import android.widget.RemoteViews; import com.android.launcher3.DragLayer.TouchCompleteListener; +import java.util.ArrayList; + /** * {@inheritDoc} */ @@ -43,6 +47,8 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc private float mSlop; + private boolean mChildrenFocused; + public LauncherAppWidgetHostView(Context context) { super(context); mContext = context; @@ -51,6 +57,8 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDragLayer = ((Launcher) context).getDragLayer(); setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + + setBackgroundResource(R.drawable.widget_internal_focus_bg); } @Override @@ -175,6 +183,108 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc @Override public int getDescendantFocusability() { - return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS + : ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE + && event.getAction() == KeyEvent.ACTION_UP) { + mChildrenFocused = false; + requestFocus(); + return true; + } + return super.dispatchKeyEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { + event.startTracking(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.isTracking()) { + if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { + mChildrenFocused = true; + ArrayList focusableChildren = getFocusables(FOCUS_FORWARD); + focusableChildren.remove(this); + int childrenCount = focusableChildren.size(); + switch (childrenCount) { + case 0: + mChildrenFocused = false; + break; + case 1: { + if (getTag() instanceof ItemInfo) { + ItemInfo item = (ItemInfo) getTag(); + if (item.spanX == 1 && item.spanY == 1) { + focusableChildren.get(0).performClick(); + mChildrenFocused = false; + return true; + } + } + // continue; + } + default: + focusableChildren.get(0).requestFocus(); + return true; + } + } + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + if (gainFocus) { + mChildrenFocused = false; + dispatchChildFocus(false); + } + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + dispatchChildFocus(focused != null); + if (focused != null) { + focused.setFocusableInTouchMode(false); + } + } + + @Override + public void clearChildFocus(View child) { + super.clearChildFocus(child); + dispatchChildFocus(false); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + return mChildrenFocused; + } + + private void dispatchChildFocus(boolean childIsFocused) { + // The host view's background changes when selected, to indicate the focus is inside. + setSelected(childIsFocused); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + try { + super.onLayout(changed, left, top, right, bottom); + } catch (final RuntimeException e) { + post(new Runnable() { + @Override + public void run() { + // Update the widget with 0 Layout id, to reset the view to error view. + updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); + } + }); + } } } diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index 882f7e2026..55edf45d29 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -123,17 +123,11 @@ public class LauncherAppWidgetInfo extends ItemInfo { */ void onBindAppWidget(Launcher launcher) { if (!mHasNotifiedInitialWidgetSizeChanged) { - notifyWidgetSizeChanged(launcher); + AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY); + mHasNotifiedInitialWidgetSizeChanged = true; } } - /** - * Trigger an update callback to the widget to notify it that its size has changed. - */ - void notifyWidgetSizeChanged(Launcher launcher) { - AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY); - mHasNotifiedInitialWidgetSizeChanged = true; - } @Override public String toString() { diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java index 71a08a8536..28d805269a 100644 --- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -59,7 +59,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { initSpans(); } - private void initSpans() { + public void initSpans() { LauncherAppState app = LauncherAppState.getInstance(); InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index 8eb4e63694..1703e41672 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -20,25 +20,26 @@ import android.app.backup.BackupAgentHelper; import android.app.backup.BackupDataInput; import android.app.backup.BackupManager; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.os.ParcelFileDescriptor; import android.util.Log; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import java.io.IOException; public class LauncherBackupAgentHelper extends BackupAgentHelper { - private static final String TAG = "LauncherBackupAgentHelper"; + private static final String TAG = "LauncherBAHelper"; + + private static final String KEY_LAST_NOTIFIED_TIME = "backup_manager_last_notified"; private static final String LAUNCHER_DATA_PREFIX = "L"; static final boolean VERBOSE = false; static final boolean DEBUG = false; - private static BackupManager sBackupManager; - /** * Notify the backup manager that out database is dirty. * @@ -47,10 +48,29 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { * @param context application context */ public static void dataChanged(Context context) { - if (sBackupManager == null) { - sBackupManager = new BackupManager(context); + dataChanged(context, 0); + } + + /** + * Notify the backup manager that out database is dirty. + * + *

This does not force an immediate backup. + * + * @param context application context + * @param throttleMs duration in ms for which two consecutive calls to backup manager should + * not be made. + */ + public static void dataChanged(Context context, long throttleMs) { + SharedPreferences prefs = Utilities.getPrefs(context); + long now = System.currentTimeMillis(); + long lastTime = prefs.getLong(KEY_LAST_NOTIFIED_TIME, 0); + + // User can manually change the system time, which could lead to now < lastTime. + // Re-backup in that case, as the backup will have a wrong lastModifiedTime. + if (now < lastTime || now >= (lastTime + throttleMs)) { + BackupManager.dataChanged(context.getPackageName()); + prefs.edit().putLong(KEY_LAST_NOTIFIED_TIME, now).apply(); } - sBackupManager.dataChanged(); } private LauncherBackupHelper mHelper; @@ -91,18 +111,16 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { if (hasData && mHelper.restoreSuccessful) { LauncherAppState.getLauncherProvider().clearFlagEmptyDbCreated(); - LauncherClings.synchonouslyMarkFirstRunClingDismissed(this); + LauncherClings.markFirstRunClingDismissed(this); // Rank was added in v4. if (mHelper.restoredBackupVersion <= 3) { LauncherAppState.getLauncherProvider().updateFolderItemsRank(); } - if (MigrateFromRestoreTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) { - MigrateFromRestoreTask.markForMigration(getApplicationContext(), - (int) mHelper.migrationCompatibleProfileData.desktopCols, - (int) mHelper.migrationCompatibleProfileData.desktopRows, - mHelper.widgetSizes); + if (GridSizeMigrationTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) { + GridSizeMigrationTask.markForMigration(getApplicationContext(), + mHelper.widgetSizes, mHelper.migrationCompatibleProfileData); } LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities(); diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 48d9351ab5..ea67ac89b4 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -41,18 +41,19 @@ import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.WorkspaceScreens; -import com.android.launcher3.backup.BackupProtos; -import com.android.launcher3.backup.BackupProtos.CheckedMessage; -import com.android.launcher3.backup.BackupProtos.DeviceProfieData; -import com.android.launcher3.backup.BackupProtos.Favorite; -import com.android.launcher3.backup.BackupProtos.Journal; -import com.android.launcher3.backup.BackupProtos.Key; -import com.android.launcher3.backup.BackupProtos.Resource; -import com.android.launcher3.backup.BackupProtos.Screen; -import com.android.launcher3.backup.BackupProtos.Widget; +import com.android.launcher3.backup.nano.BackupProtos; +import com.android.launcher3.backup.nano.BackupProtos.CheckedMessage; +import com.android.launcher3.backup.nano.BackupProtos.DeviceProfieData; +import com.android.launcher3.backup.nano.BackupProtos.Favorite; +import com.android.launcher3.backup.nano.BackupProtos.Journal; +import com.android.launcher3.backup.nano.BackupProtos.Key; +import com.android.launcher3.backup.nano.BackupProtos.Resource; +import com.android.launcher3.backup.nano.BackupProtos.Screen; +import com.android.launcher3.backup.nano.BackupProtos.Widget; +import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; @@ -315,14 +316,13 @@ public class LauncherBackupHelper implements BackupHelper { return true; } - if (MigrateFromRestoreTask.ENABLED && - (oldProfile.desktopCols - currentProfile.desktopCols <= 1) && - (oldProfile.desktopRows - currentProfile.desktopRows <= 1)) { - // Allow desktop migration when row and/or column count contracts by 1. - + if (GridSizeMigrationTask.ENABLED) { + // One time migrate the workspace when launcher starts. migrationCompatibleProfileData = initDeviceProfileData(mIdp); migrationCompatibleProfileData.desktopCols = oldProfile.desktopCols; migrationCompatibleProfileData.desktopRows = oldProfile.desktopRows; + migrationCompatibleProfileData.hotseatCount = oldProfile.hotseatCount; + migrationCompatibleProfileData.allappsRank = oldProfile.allappsRank; return true; } return false; @@ -661,12 +661,14 @@ public class LauncherBackupHelper implements BackupHelper { + getUserSelectionArg(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, where, null, null); + AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext); try { cursor.moveToPosition(-1); while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); final ComponentName provider = ComponentName.unflattenFromString(providerName); + Key key = null; String backupKey = null; if (provider != null) { @@ -685,11 +687,14 @@ public class LauncherBackupHelper implements BackupHelper { } else if (backupKey != null) { if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount); if (backupWidgetCount < MAX_WIDGETS_PER_PASS) { - if (DEBUG) Log.d(TAG, "saving widget " + backupKey); - UserHandleCompat user = UserHandleCompat.myUserHandle(); - writeRowToBackup(key, packWidget(dpi, provider, user), data); - mKeys.add(key); - backupWidgetCount ++; + LauncherAppWidgetProviderInfo widgetInfo = widgetManager + .getLauncherAppWidgetInfo(cursor.getInt(APPWIDGET_ID_INDEX)); + if (widgetInfo != null) { + if (DEBUG) Log.d(TAG, "saving widget " + backupKey); + writeRowToBackup(key, packWidget(dpi, widgetInfo), data); + mKeys.add(key); + backupWidgetCount ++; + } } else { if (VERBOSE) Log.v(TAG, "deferring widget backup " + backupKey); // too many widgets for this pass, request another. @@ -768,13 +773,10 @@ public class LauncherBackupHelper implements BackupHelper { try { Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT)); if (key.checksum != checkKey(key)) { - key = null; throw new InvalidBackupException("invalid key read from stream" + backupKey); } return key; - } catch (InvalidProtocolBufferNanoException e) { - throw new InvalidBackupException(e); - } catch (IllegalArgumentException e) { + } catch (InvalidProtocolBufferNanoException | IllegalArgumentException e) { throw new InvalidBackupException(e); } } @@ -1008,16 +1010,14 @@ public class LauncherBackupHelper implements BackupHelper { } /** Serialize a widget for persistence, including a checksum wrapper. */ - private Widget packWidget(int dpi, ComponentName provider, UserHandleCompat user) { - final LauncherAppWidgetProviderInfo info = - LauncherModel.getProviderInfo(mContext, provider, user); + private Widget packWidget(int dpi, LauncherAppWidgetProviderInfo info) { Widget widget = new Widget(); - widget.provider = provider.flattenToShortString(); + widget.provider = info.provider.flattenToShortString(); widget.label = info.label; widget.configure = info.configure != null; if (info.icon != 0) { widget.icon = new Resource(); - Drawable fullResIcon = mIconCache.getFullResIcon(provider.getPackageName(), info.icon); + Drawable fullResIcon = mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon); Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext); widget.icon.data = Utilities.flattenBitmap(icon); widget.icon.dpi = dpi; @@ -1026,7 +1026,6 @@ public class LauncherBackupHelper implements BackupHelper { Point spans = info.getMinSpans(mIdp, mContext); widget.minSpanX = spans.x; widget.minSpanY = spans.y; - return widget; } @@ -1131,9 +1130,8 @@ public class LauncherBackupHelper implements BackupHelper { * @param journal a Journal protocol buffer */ private void writeJournal(ParcelFileDescriptor newState, Journal journal) { - FileOutputStream outStream = null; try { - outStream = new FileOutputStream(newState.getFileDescriptor()); + FileOutputStream outStream = new FileOutputStream(newState.getFileDescriptor()); final byte[] journalBytes = writeCheckedBytes(journal); outStream.write(journalBytes); if (VERBOSE) Log.v(TAG, "wrote " + journalBytes.length + " bytes of journal"); diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index e34bd57fdd..772fbf358f 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -77,12 +77,14 @@ public interface LauncherCallbacks { public boolean providesSearch(); public boolean startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds); + @Deprecated public boolean startSearchFromAllApps(String query); @Deprecated public void startVoice(); public boolean hasCustomContentToLeft(); public void populateCustomContentContainer(); public View getQsbBar(); + public Bundle getAdditionalSearchWidgetOptions(); /* * Extensions points for adding / replacing some other aspects of the Launcher experience. @@ -99,6 +101,9 @@ public interface LauncherCallbacks { public boolean isLauncherPreinstalled(); public AllAppsSearchBarController getAllAppsSearchBarController(); public List getPredictedApps(); + public static final int SEARCH_BAR_HEIGHT_NORMAL = 0, SEARCH_BAR_HEIGHT_TALL = 1; + /** Must return one of {@link #SEARCH_BAR_HEIGHT_NORMAL} or {@link #SEARCH_BAR_HEIGHT_TALL} */ + public int getSearchBarHeight(); /** * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup, diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index 18fe8ef868..5c1e83036f 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -73,9 +73,7 @@ class LauncherClings implements OnClickListener { LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); // Set the flag to skip the folder cling - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); + SharedPreferences.Editor editor = Utilities.getPrefs(mLauncher).edit(); editor.putBoolean(Launcher.USER_HAS_MIGRATED, true); editor.apply(); // Disable the migration cling @@ -253,11 +251,9 @@ class LauncherClings implements OnClickListener { !sharedPrefs.getBoolean(MIGRATION_CLING_DISMISSED_KEY, false); } - public static void synchonouslyMarkFirstRunClingDismissed(Context ctx) { - SharedPreferences prefs = ctx.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(WORKSPACE_CLING_DISMISSED_KEY, true); - editor.commit(); + public static void markFirstRunClingDismissed(Context ctx) { + Utilities.getPrefs(ctx).edit() + .putBoolean(WORKSPACE_CLING_DISMISSED_KEY, true) + .apply(); } } diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index c08cd0bf52..6ce2293829 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -34,7 +34,7 @@ public class LauncherFiles { WALLPAPER_CROP_PREFERENCES_KEY + XML, WALLPAPER_IMAGES_DB, WIDGET_PREVIEWS_DB, - MANAGED_USER_PREFERENCES_KEY, + MANAGED_USER_PREFERENCES_KEY + XML, APP_ICONS_DB)); // TODO: Delete these files on upgrade diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index b5922c6a38..c8860e3ed0 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -17,7 +17,6 @@ package com.android.launcher3; import android.app.SearchManager; -import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -34,14 +33,12 @@ import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; -import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Parcelable; import android.os.Process; import android.os.SystemClock; -import android.os.TransactionTooLargeException; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; @@ -55,12 +52,15 @@ import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.CursorIconInfo; +import com.android.launcher3.util.FlagOp; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.PackageManagerHelper; +import com.android.launcher3.util.StringFilter; import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; @@ -68,7 +68,6 @@ import java.net.URISyntaxException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -98,7 +97,6 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; - @Thunk final boolean mAppsCanBeOnRemoveableStorage; private final boolean mOldContentProviderExists; @Thunk final LauncherAppState mApp; @@ -137,9 +135,9 @@ public class LauncherModel extends BroadcastReceiver @Thunk WeakReference mCallbacks; // < only access in worker thread > - AllAppsList mBgAllAppsList; + private final AllAppsList mBgAllAppsList; // Entire list of widgets. - WidgetsModel mBgWidgetsModel; + private final WidgetsModel mBgWidgetsModel; // The lock that must be acquired before referencing any static bg data structures. Unlike // other locks, this one can generally be held long-term because we never expect any of these @@ -166,9 +164,6 @@ public class LauncherModel extends BroadcastReceiver // sBgWorkspaceScreens is the ordered set of workspace screens. static final ArrayList sBgWorkspaceScreens = new ArrayList(); - // sBgWidgetProviders is the set of widget providers including custom internal widgets - public static HashMap sBgWidgetProviders; - // sPendingPackages is a set of packages which could be on sdcard and are not available yet static final HashMap> sPendingPackages = new HashMap>(); @@ -201,9 +196,12 @@ public class LauncherModel extends BroadcastReceiver ArrayList removed, UserHandleCompat user); public void bindWidgetsRestored(ArrayList widgets); public void bindRestoreItemsChange(HashSet updates); - public void bindComponentsRemoved(ArrayList packageNames, - ArrayList appInfos, UserHandleCompat user, int reason); - public void bindAllPackages(WidgetsModel model); + public void bindWorkspaceComponentsRemoved( + HashSet packageNames, HashSet components, + UserHandleCompat user); + public void bindAppInfosRemoved(ArrayList appInfos); + public void notifyWidgetProvidersChanged(); + public void bindWidgetsModel(WidgetsModel model); public void bindSearchProviderChanged(); public boolean isAllAppsButtonRank(int rank); public void onPageBoundSynchronously(int page); @@ -217,7 +215,6 @@ public class LauncherModel extends BroadcastReceiver LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) { Context context = app.getContext(); - mAppsCanBeOnRemoveableStorage = Environment.isExternalStorageRemovable(); String oldProvider = context.getString(R.string.old_launcher_provider_uri); // This may be the same as MIGRATE_AUTHORITY, or it may be replaced by a different // resource string. @@ -1068,8 +1065,6 @@ public class LauncherModel extends BroadcastReceiver /** * Removes the specified item from the database - * @param context - * @param item */ public static void deleteItemFromDatabase(Context context, final ItemInfo item) { ArrayList items = new ArrayList(); @@ -1079,8 +1074,6 @@ public class LauncherModel extends BroadcastReceiver /** * Removes the specified items from the database - * @param context - * @param item */ static void deleteItemsFromDatabase(Context context, final ArrayList items) { final ContentResolver cr = context.getContentResolver(); @@ -1171,9 +1164,9 @@ public class LauncherModel extends BroadcastReceiver } /** - * Remove the contents of the specified folder from the database + * Remove the specified folder and all its contents from the database. */ - public static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { + public static void deleteFolderAndContentsFromDatabase(Context context, final FolderInfo info) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { @@ -1235,20 +1228,8 @@ public class LauncherModel extends BroadcastReceiver @Override public void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing) { - if (!replacing) { - enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packageNames, - user)); - if (mAppsCanBeOnRemoveableStorage) { - // Only rebind if we support removable storage. It catches the - // case where - // apps on the external sd card need to be reloaded - startLoaderFromBackground(); - } - } else { - // If we are replacing then just update the packages in the list - enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, - packageNames, user)); - } + enqueuePackageUpdated( + new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, packageNames, user)); } @Override @@ -1261,6 +1242,20 @@ public class LauncherModel extends BroadcastReceiver } } + @Override + public void onPackagesSuspended(String[] packageNames, UserHandleCompat user) { + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_SUSPEND, packageNames, + user)); + } + + @Override + public void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user) { + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_UNSUSPEND, packageNames, + user)); + } + /** * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and * ACTION_PACKAGE_CHANGED. @@ -1282,6 +1277,14 @@ public class LauncherModel extends BroadcastReceiver || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { UserManagerCompat.getInstance(context).enableAndResetCache(); forceReload(); + } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || + LauncherAppsCompat.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { + UserHandleCompat user = UserHandleCompat.fromIntent(intent); + if (user != null) { + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, + new String[0], user)); + } } } @@ -1733,23 +1736,10 @@ public class LauncherModel extends BroadcastReceiver int countX = profile.numColumns; int countY = profile.numRows; - if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) { - long migrationStartTime = System.currentTimeMillis(); - Log.v(TAG, "Starting workspace migration after restore"); - try { - MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext); - // Clear the flags before starting the task, so that we do not run the task - // again, in case there was an uncaught error. - MigrateFromRestoreTask.clearFlags(mContext); - task.execute(); - } catch (Exception e) { - Log.e(TAG, "Error during grid migration", e); - - // Clear workspace. - mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE; - } - Log.v(TAG, "Workspace migration completed in " - + (System.currentTimeMillis() - migrationStartTime)); + if (GridSizeMigrationTask.ENABLED && + !GridSizeMigrationTask.migrateGridIfNeeded(mContext)) { + // Migration failed. Clear workspace. + mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE; } if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) { @@ -1773,8 +1763,8 @@ public class LauncherModel extends BroadcastReceiver .getInstance(mContext).updateAndGetActiveSessionCache(); sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); - final ArrayList itemsToRemove = new ArrayList(); - final ArrayList restoredRows = new ArrayList(); + final ArrayList itemsToRemove = new ArrayList<>(); + final ArrayList restoredRows = new ArrayList<>(); final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI; if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri); final Cursor c = contentResolver.query(contentUri, null, null, null, null); @@ -1783,6 +1773,7 @@ public class LauncherModel extends BroadcastReceiver // Load workspace in reverse order to ensure that latest items are loaded first (and // before any earlier duplicates) final LongArrayMap occupied = new LongArrayMap<>(); + HashMap widgetProvidersMap = null; try { final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); @@ -1819,8 +1810,11 @@ public class LauncherModel extends BroadcastReceiver final CursorIconInfo cursorIconInfo = new CursorIconInfo(c); final LongSparseArray allUsers = new LongSparseArray<>(); + final LongSparseArray quietMode = new LongSparseArray<>(); for (UserHandleCompat user : mUserManager.getUserProfiles()) { - allUsers.put(mUserManager.getSerialNumberForUser(user), user); + long serialNo = mUserManager.getSerialNumberForUser(user); + allUsers.put(serialNo, user); + quietMode.put(serialNo, mUserManager.isQuietModeEnabled(user)); } ShortcutInfo info; @@ -1831,6 +1825,7 @@ public class LauncherModel extends BroadcastReceiver long serialNumber; Intent intent; UserHandleCompat user; + String targetPackage; while (!mStopped && c.moveToNext()) { try { @@ -1849,6 +1844,7 @@ public class LauncherModel extends BroadcastReceiver int promiseType = c.getInt(restoredIndex); int disabledState = 0; boolean itemReplaced = false; + targetPackage = null; if (user == null) { // User has been deleted remove the item. itemsToRemove.add(id); @@ -1862,6 +1858,9 @@ public class LauncherModel extends BroadcastReceiver cn.getPackageName(), user); boolean validComponent = validPkg && launcherApps.isActivityEnabledForProfile(cn, user); + if (validPkg) { + targetPackage = cn.getPackageName(); + } if (validComponent) { if (restored) { @@ -1869,6 +1868,9 @@ public class LauncherModel extends BroadcastReceiver restoredRows.add(id); restored = false; } + if (quietMode.get(serialNumber)) { + disabledState = ShortcutInfo.FLAG_DISABLED_QUIET_USER; + } } else if (validPkg) { intent = null; if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { @@ -1937,9 +1939,8 @@ public class LauncherModel extends BroadcastReceiver itemsToRemove.add(id); continue; } - } else if (launcherApps.isAppEnabled( - manager, cn.getPackageName(), - PackageManager.GET_UNINSTALLED_PACKAGES)) { + } else if (PackageManagerHelper.isAppOnSdcard( + manager, cn.getPackageName())) { // Package is present but not available. allowMissingTarget = true; disabledState = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE; @@ -1982,7 +1983,7 @@ public class LauncherModel extends BroadcastReceiver if (itemReplaced) { if (user.equals(UserHandleCompat.myUserHandle())) { - info = getAppShortcutInfo(manager, intent, user, context, null, + info = getAppShortcutInfo(intent, user, context, null, cursorIconInfo.iconIndex, titleIndex, false, useLowResIcon); } else { @@ -2005,12 +2006,17 @@ public class LauncherModel extends BroadcastReceiver } } else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = getAppShortcutInfo(manager, intent, user, context, c, + info = getAppShortcutInfo(intent, user, context, c, cursorIconInfo.iconIndex, titleIndex, allowMissingTarget, useLowResIcon); } else { info = getShortcutInfo(c, context, titleIndex, cursorIconInfo); + // Shortcuts are only available on the primary profile + if (PackageManagerHelper.isAppSuspended(manager, targetPackage)) { + disabledState |= ShortcutInfo.FLAG_DISABLED_SUSPENDED; + } + // App shortcuts that used to be automatically added to Launcher // didn't always have the correct intent flags set, so do that // here @@ -2038,7 +2044,7 @@ public class LauncherModel extends BroadcastReceiver if (info.promisedIntent != null) { info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); } - info.isDisabled = disabledState; + info.isDisabled |= disabledState; if (isSafeMode && !Utilities.isSystemApp(context, intent)) { info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE; } @@ -2141,10 +2147,14 @@ public class LauncherModel extends BroadcastReceiver final boolean wasProviderReady = (restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0; - final LauncherAppWidgetProviderInfo provider = - LauncherModel.getProviderInfo(context, + if (widgetProvidersMap == null) { + widgetProvidersMap = AppWidgetManagerCompat + .getInstance(mContext).getAllProvidersMap(); + } + final AppWidgetProviderInfo provider = widgetProvidersMap.get( + new ComponentKey( ComponentName.unflattenFromString(savedProvider), - user); + user)); final boolean isProviderReady = isValidProvider(provider); if (!isSafeMode && !customWidget && @@ -2387,7 +2397,7 @@ public class LauncherModel extends BroadcastReceiver Collections.sort(allWorkspaceItems, new Comparator() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { - return (int) (lhs.container - rhs.container); + return Utilities.longCompare(lhs.container, rhs.container); } }); for (ItemInfo info : allWorkspaceItems) { @@ -2469,7 +2479,7 @@ public class LauncherModel extends BroadcastReceiver lhs.cellY * cellCountX + lhs.cellX); long rr = (rhs.container * containerOffset + rhs.screenId * screenOffset + rhs.cellY * cellCountX + rhs.cellX); - return (int) (lr - rr); + return Utilities.longCompare(lr, rr); } }); } @@ -2757,18 +2767,16 @@ public class LauncherModel extends BroadcastReceiver @SuppressWarnings("unchecked") final ArrayList list = (ArrayList) mBgAllAppsList.data.clone(); - final WidgetsModel widgetList = mBgWidgetsModel.clone(); Runnable r = new Runnable() { public void run() { final long t = SystemClock.uptimeMillis(); final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAllApplications(list); - callbacks.bindAllPackages(widgetList); } if (DEBUG_LOADERS) { Log.d(TAG, "bound all " + list.size() + " apps from cache in " - + (SystemClock.uptimeMillis()-t) + "ms"); + + (SystemClock.uptimeMillis() - t) + "ms"); } } }; @@ -2808,12 +2816,12 @@ public class LauncherModel extends BroadcastReceiver if (apps == null || apps.isEmpty()) { return; } - + boolean quietMode = mUserManager.isQuietModeEnabled(user); // Create the ApplicationInfos for (int i = 0; i < apps.size(); i++) { LauncherActivityInfoCompat app = apps.get(i); // This builds the icon bitmaps. - mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache)); + mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, quietMode)); } final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user); @@ -2856,7 +2864,7 @@ public class LauncherModel extends BroadcastReceiver callbacks.bindAllApplications(added); if (DEBUG_LOADERS) { Log.d(TAG, "bound " + added.size() + " apps in " - + (SystemClock.uptimeMillis() - bindTime) + "ms"); + + (SystemClock.uptimeMillis() - bindTime) + "ms"); } } else { Log.i(TAG, "not binding apps: no Launcher activity"); @@ -2865,8 +2873,6 @@ public class LauncherModel extends BroadcastReceiver }); // Cleanup any data stored for a deleted user. ManagedProfileHeuristic.processAllUsers(profiles, mContext); - - loadAndBindWidgetsAndShortcuts(tryGetCallbacks(oldCallbacks), true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " + (SystemClock.uptimeMillis() - loadTime) + "ms"); @@ -2933,9 +2939,6 @@ public class LauncherModel extends BroadcastReceiver } }); } - - // Reload widget list. No need to refresh, as we only want to update the icons and labels. - loadAndBindWidgetsAndShortcuts(callbacks, false); } void enqueuePackageUpdated(PackageUpdatedTask task) { @@ -2958,10 +2961,7 @@ public class LauncherModel extends BroadcastReceiver packagesUnavailable.clear(); for (String pkg : entry.getValue()) { if (!launcherApps.isPackageEnabledForProfile(pkg, user)) { - boolean packageOnSdcard = launcherApps.isAppEnabled( - manager, pkg, PackageManager.GET_UNINSTALLED_PACKAGES); - if (packageOnSdcard) { - Launcher.addDumpLog(TAG, "Package found on sd-card: " + pkg, true); + if (PackageManagerHelper.isAppOnSdcard(manager, pkg)) { packagesUnavailable.add(pkg); } else { Launcher.addDumpLog(TAG, "Package not found: " + pkg, true); @@ -2993,7 +2993,9 @@ public class LauncherModel extends BroadcastReceiver public static final int OP_UPDATE = 2; public static final int OP_REMOVE = 3; // uninstlled public static final int OP_UNAVAILABLE = 4; // external media unmounted - + public static final int OP_SUSPEND = 5; // package suspended + public static final int OP_UNSUSPEND = 6; // package unsuspended + public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable public PackageUpdatedTask(int op, String[] packages, UserHandleCompat user) { mOp = op; @@ -3010,6 +3012,8 @@ public class LauncherModel extends BroadcastReceiver final String[] packages = mPackages; final int N = packages.length; + FlagOp flagOp = FlagOp.NO_OP; + StringFilter pkgFilter = StringFilter.of(new HashSet<>(Arrays.asList(packages))); switch (mOp) { case OP_ADD: { for (int i=0; i removedApps = new ArrayList(); if (mBgAllAppsList.added.size() > 0) { - added = new ArrayList(mBgAllAppsList.added); + added = new ArrayList<>(mBgAllAppsList.added); mBgAllAppsList.added.clear(); } if (mBgAllAppsList.modified.size() > 0) { - modified = new ArrayList(mBgAllAppsList.modified); + modified = new ArrayList<>(mBgAllAppsList.modified); mBgAllAppsList.modified.clear(); } if (mBgAllAppsList.removed.size() > 0) { @@ -3069,14 +3092,7 @@ public class LauncherModel extends BroadcastReceiver mBgAllAppsList.removed.clear(); } - final Callbacks callbacks = getCallback(); - if (callbacks == null) { - Log.w(TAG, "Nobody to tell about the new app. Launcher is probably loading."); - return; - } - - final HashMap addedOrUpdatedApps = - new HashMap(); + final HashMap addedOrUpdatedApps = new HashMap<>(); if (added != null) { addAppsToAllApps(context, added); @@ -3086,6 +3102,7 @@ public class LauncherModel extends BroadcastReceiver } if (modified != null) { + final Callbacks callbacks = getCallback(); final ArrayList modifiedFinal = modified; for (AppInfo ai : modified) { addedOrUpdatedApps.put(ai.componentName, ai); @@ -3102,12 +3119,11 @@ public class LauncherModel extends BroadcastReceiver } // Update shortcut infos - if (mOp == OP_ADD || mOp == OP_UPDATE) { + if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { final ArrayList updatedShortcuts = new ArrayList(); final ArrayList removedShortcuts = new ArrayList(); final ArrayList widgets = new ArrayList(); - HashSet packageSet = new HashSet(Arrays.asList(packages)); synchronized (sBgLock) { for (ItemInfo info : sBgItemsIdMap) { if (info instanceof ShortcutInfo && mUser.equals(info.user)) { @@ -3117,7 +3133,7 @@ public class LauncherModel extends BroadcastReceiver // Update shortcuts which use iconResource. if ((si.iconResource != null) - && packageSet.contains(si.iconResource.packageName)) { + && pkgFilter.matches(si.iconResource.packageName)) { Bitmap icon = Utilities.createIconBitmap( si.iconResource.packageName, si.iconResource.resourceName, context); @@ -3129,7 +3145,7 @@ public class LauncherModel extends BroadcastReceiver } ComponentName cn = si.getTargetComponent(); - if (cn != null && packageSet.contains(cn.getPackageName())) { + if (cn != null && pkgFilter.matches(cn.getPackageName())) { AppInfo appInfo = addedOrUpdatedApps.get(cn); if (si.isPromise()) { @@ -3177,9 +3193,9 @@ public class LauncherModel extends BroadcastReceiver infoUpdated = true; } - if ((si.isDisabled & ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE) != 0) { - // Since package was just updated, the target must be available now. - si.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE; + int oldDisabledFlags = si.isDisabled; + si.isDisabled = flagOp.apply(si.isDisabled); + if (si.isDisabled != oldDisabledFlags) { shortcutUpdated = true; } } @@ -3190,11 +3206,11 @@ public class LauncherModel extends BroadcastReceiver if (infoUpdated) { updateItemInDatabase(context, si); } - } else if (info instanceof LauncherAppWidgetInfo) { + } else if (info instanceof LauncherAppWidgetInfo && mOp == OP_ADD) { LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info; if (mUser.equals(widgetInfo.user) && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) - && packageSet.contains(widgetInfo.providerName.getPackageName())) { + && pkgFilter.matches(widgetInfo.providerName.getPackageName())) { widgetInfo.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; @@ -3212,6 +3228,7 @@ public class LauncherModel extends BroadcastReceiver } if (!updatedShortcuts.isEmpty() || !removedShortcuts.isEmpty()) { + final Callbacks callbacks = getCallback(); mHandler.post(new Runnable() { public void run() { @@ -3227,6 +3244,7 @@ public class LauncherModel extends BroadcastReceiver } } if (!widgets.isEmpty()) { + final Callbacks callbacks = getCallback(); mHandler.post(new Runnable() { public void run() { Callbacks cb = getCallback(); @@ -3238,171 +3256,104 @@ public class LauncherModel extends BroadcastReceiver } } - final ArrayList removedPackageNames = - new ArrayList(); - if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE) { + final HashSet removedPackages = new HashSet<>(); + final HashSet removedComponents = new HashSet<>(); + if (mOp == OP_REMOVE) { // Mark all packages in the broadcast to be removed - removedPackageNames.addAll(Arrays.asList(packages)); + Collections.addAll(removedPackages, packages); + + // No need to update the removedComponents as + // removedPackages is a super-set of removedComponents } else if (mOp == OP_UPDATE) { // Mark disabled packages in the broadcast to be removed for (int i=0; i infos = getItemInfoForComponentName(a.componentName, mUser); - deleteItemsFromDatabase(context, infos); - } - removeReason = 0; + if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { + for (String pn : removedPackages) { + deletePackageFromDatabase(context, pn, mUser); + } + for (ComponentName cn : removedComponents) { + deleteItemsFromDatabase(context, getItemInfoForComponentName(cn, mUser)); } // Remove any queued items from the install queue - InstallShortcutReceiver.removeFromInstallQueue(context, removedPackageNames, mUser); + InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser); + // Call the components-removed callback + final Callbacks callbacks = getCallback(); mHandler.post(new Runnable() { public void run() { Callbacks cb = getCallback(); if (callbacks == cb && cb != null) { - callbacks.bindComponentsRemoved( - removedPackageNames, removedApps, mUser, removeReason); + callbacks.bindWorkspaceComponentsRemoved( + removedPackages, removedComponents, mUser); } } }); } - // Update widgets - if (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE) { - // Always refresh for a package event on secondary user - boolean needToRefresh = !mUser.equals(UserHandleCompat.myUserHandle()); - - // Refresh widget list, if the package already had a widget. - synchronized (sBgLock) { - if (sBgWidgetProviders != null) { - HashSet pkgSet = new HashSet<>(); - Collections.addAll(pkgSet, mPackages); - - for (ComponentKey key : sBgWidgetProviders.keySet()) { - needToRefresh |= key.user.equals(mUser) && - pkgSet.contains(key.componentName.getPackageName()); + if (!removedApps.isEmpty()) { + // Remove corresponding apps from All-Apps + final Callbacks callbacks = getCallback(); + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = getCallback(); + if (callbacks == cb && cb != null) { + callbacks.bindAppInfosRemoved(removedApps); } } - } - - if (!needToRefresh && mOp != OP_REMOVE) { - // Refresh widget list, if there is any newly added widget - PackageManager pm = context.getPackageManager(); - for (String pkg : mPackages) { - needToRefresh |= !pm.queryBroadcastReceivers( - new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE) - .setPackage(pkg), 0).isEmpty(); - } - } - - loadAndBindWidgetsAndShortcuts(callbacks, needToRefresh); + }); } - // Write all the logs to disk - mHandler.post(new Runnable() { - public void run() { - Callbacks cb = getCallback(); - if (callbacks == cb && cb != null) { - callbacks.dumpLogsToLocalData(); + // Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to + // get widget update signals. + if (!Utilities.ATLEAST_MARSHMALLOW && + (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) { + final Callbacks callbacks = getCallback(); + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = getCallback(); + if (callbacks == cb && cb != null) { + callbacks.notifyWidgetProvidersChanged(); + } } - } - }); - } - } - - public static List getWidgetProviders(Context context, - boolean refresh) { - ArrayList results = - new ArrayList(); - try { - synchronized (sBgLock) { - if (sBgWidgetProviders == null || refresh) { - HashMap tmpWidgetProviders - = new HashMap<>(); - AppWidgetManagerCompat wm = AppWidgetManagerCompat.getInstance(context); - LauncherAppWidgetProviderInfo info; - - List widgets = wm.getAllProviders(); - for (AppWidgetProviderInfo pInfo : widgets) { - info = LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo); - UserHandleCompat user = wm.getUser(info); - tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); - } - - Collection customWidgets = Launcher.getCustomAppWidgets().values(); - for (CustomAppWidget widget : customWidgets) { - info = new LauncherAppWidgetProviderInfo(context, widget); - UserHandleCompat user = wm.getUser(info); - tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); - } - // Replace the global list at the very end, so that if there is an exception, - // previously loaded provider list is used. - sBgWidgetProviders = tmpWidgetProviders; - } - results.addAll(sBgWidgetProviders.values()); - return results; - } - } catch (Exception e) { - if (e.getCause() instanceof TransactionTooLargeException) { - // the returned value may be incomplete and will not be refreshed until the next - // time Launcher starts. - // TODO: after figuring out a repro step, introduce a dirty bit to check when - // onResume is called to refresh the widget provider list. - synchronized (sBgLock) { - if (sBgWidgetProviders != null) { - results.addAll(sBgWidgetProviders.values()); - } - return results; - } - } else { - throw e; + }); } } } - public static LauncherAppWidgetProviderInfo getProviderInfo(Context ctx, ComponentName name, - UserHandleCompat user) { - synchronized (sBgLock) { - if (sBgWidgetProviders == null) { - getWidgetProviders(ctx, false /* refresh */); + private void bindWidgetsModel(final Callbacks callbacks, final WidgetsModel model) { + mHandler.post(new Runnable() { + @Override + public void run() { + Callbacks cb = getCallback(); + if (callbacks == cb && cb != null) { + callbacks.bindWidgetsModel(model); + } } - return sBgWidgetProviders.get(new ComponentKey(name, user)); - } + }); } - public void loadAndBindWidgetsAndShortcuts(final Callbacks callbacks, final boolean refresh) { - + public void refreshAndBindWidgetsAndShortcuts( + final Callbacks callbacks, final boolean bindFirst) { runOnWorkerThread(new Runnable() { @Override public void run() { - updateWidgetsModel(refresh); - final WidgetsModel model = mBgWidgetsModel.clone(); - - mHandler.post(new Runnable() { - @Override - public void run() { - Callbacks cb = getCallback(); - if (callbacks == cb && cb != null) { - callbacks.bindAllPackages(model); - } - } - }); + if (bindFirst && !mBgWidgetsModel.isEmpty()) { + bindWidgetsModel(callbacks, mBgWidgetsModel.clone()); + } + final WidgetsModel model = mBgWidgetsModel.updateAndClone(mApp.getContext()); + bindWidgetsModel(callbacks, model); // update the Widget entries inside DB on the worker thread. LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews( model.getRawList()); @@ -3410,20 +3361,6 @@ public class LauncherModel extends BroadcastReceiver }); } - /** - * Returns a list of ResolveInfos/AppWidgetInfos. - * - * @see #loadAndBindWidgetsAndShortcuts - */ - @Thunk void updateWidgetsModel(boolean refresh) { - PackageManager packageManager = mApp.getContext().getPackageManager(); - final ArrayList widgetsAndShortcuts = new ArrayList(); - widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh)); - Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); - widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); - mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts); - } - @Thunk static boolean isPackageDisabled(Context context, String packageName, UserHandleCompat user) { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); @@ -3511,7 +3448,7 @@ public class LauncherModel extends BroadcastReceiver * * If c is not null, then it will be used to fill in missing data like the title and icon. */ - public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent, + public ShortcutInfo getAppShortcutInfo(Intent intent, UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex, boolean allowMissingTarget, boolean useLowResIcon) { if (user == null) { @@ -3521,7 +3458,7 @@ public class LauncherModel extends BroadcastReceiver ComponentName componentName = intent.getComponent(); if (componentName == null) { - Log.d(TAG, "Missing component found in getShortcutInfo: " + componentName); + Log.d(TAG, "Missing component found in getShortcutInfo"); return null; } @@ -3541,6 +3478,10 @@ public class LauncherModel extends BroadcastReceiver info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon); } + if (lai != null && PackageManagerHelper.isAppSuspended(lai.getApplicationInfo())) { + info.isDisabled = ShortcutInfo.FLAG_DISABLED_SUSPENDED; + } + // from the db if (TextUtils.isEmpty(info.title) && c != null) { info.title = Utilities.trim(c.getString(titleIndex)); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 8791e9e57d..7c4b78d0be 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -57,7 +57,6 @@ import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.util.ManagedProfileHeuristic; import com.android.launcher3.util.Thunk; -import java.io.File; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; @@ -79,11 +78,15 @@ public class LauncherProvider extends ContentProvider { private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; @Thunk LauncherProviderChangeListener mListener; - @Thunk DatabaseHelper mOpenHelper; + protected DatabaseHelper mOpenHelper; @Override public boolean onCreate() { final Context context = getContext(); + // The content provider exists for the entire duration of the launcher main process and + // is the first component to get created. Initializing application context here ensures + // that LauncherAppState always exists in the main process. + LauncherAppState.setApplicationContext(context.getApplicationContext()); StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); mOpenHelper = new DatabaseHelper(context); StrictMode.setThreadPolicy(oldPolicy); @@ -110,6 +113,15 @@ public class LauncherProvider extends ContentProvider { } } + /** + * Overridden in tests + */ + protected synchronized void createDbIfNotExists() { + if (mOpenHelper == null) { + mOpenHelper = new DatabaseHelper(getContext()); + } + } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { @@ -256,21 +268,25 @@ public class LauncherProvider extends ContentProvider { switch (method) { case LauncherSettings.Settings.METHOD_GET_BOOLEAN: { Bundle result = new Bundle(); - result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, - getContext().getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) - .getBoolean(arg, extras.getBoolean( - LauncherSettings.Settings.EXTRA_DEFAULT_VALUE))); + if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(arg)) { + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, + Utilities.isAllowRotationPrefEnabled(getContext())); + } else { + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, + Utilities.getPrefs(getContext()).getBoolean(arg, extras.getBoolean( + LauncherSettings.Settings.EXTRA_DEFAULT_VALUE))); + } return result; } case LauncherSettings.Settings.METHOD_SET_BOOLEAN: { boolean value = extras.getBoolean(LauncherSettings.Settings.EXTRA_VALUE); - getContext().getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) - .edit().putBoolean(arg, value).apply(); + Utilities.getPrefs(getContext()).edit().putBoolean(arg, value).apply(); if (mListener != null) { mListener.onSettingsChanged(arg, value); } + if (extras.getBoolean(LauncherSettings.Settings.NOTIFY_BACKUP)) { + LauncherBackupAgentHelper.dataChanged(getContext()); + } Bundle result = new Bundle(); result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, value); return result; @@ -315,7 +331,10 @@ public class LauncherProvider extends ContentProvider { return folderIds; } - private void notifyListeners() { + /** + * Overridden in tests + */ + protected void notifyListeners() { // always notify the backup agent LauncherBackupAgentHelper.dataChanged(getContext()); if (mListener != null) { @@ -331,10 +350,6 @@ public class LauncherProvider extends ContentProvider { return mOpenHelper.generateNewItemId(); } - public void updateMaxItemId(long id) { - mOpenHelper.updateMaxItemId(id); - } - public long generateNewScreenId() { return mOpenHelper.generateNewScreenId(); } @@ -347,11 +362,7 @@ public class LauncherProvider extends ContentProvider { } public void clearFlagEmptyDbCreated() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE) - .edit() - .remove(EMPTY_DATABASE_CREATED) - .commit(); + Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit(); } /** @@ -362,8 +373,7 @@ public class LauncherProvider extends ContentProvider { * 4) The default configuration for the particular device */ synchronized public void loadDefaultFavoritesIfNecessary() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences sp = Utilities.getPrefs(getContext()); if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { Log.d(TAG, "loading default workspace"); @@ -463,17 +473,13 @@ public class LauncherProvider extends ContentProvider { public void deleteDatabase() { // Are you sure? (y/n) - final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - final File dbFile = new File(db.getPath()); - mOpenHelper.close(); - if (dbFile.exists()) { - SQLiteDatabase.deleteDatabase(dbFile); - } - mOpenHelper = new DatabaseHelper(getContext()); - mOpenHelper.mListener = mListener; + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); } - private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { + /** + * The class is subclassed in tests to create an in-memory db. + */ + protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { private final Context mContext; @Thunk final AppWidgetHost mAppWidgetHost; private long mMaxItemId = -1; @@ -488,6 +494,16 @@ public class LauncherProvider extends ContentProvider { mContext = context; mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); + // Table creation sometimes fails silently, which leads to a crash loop. + // This way, we will try to create a table every time after crash, so the device + // would eventually be able to recover. + if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) { + Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate"); + // This operation is a no-op if the table already exists. + addFavoritesTable(getWritableDatabase(), true); + addWorkspacesTable(getWritableDatabase(), true); + } + // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from // the DB here if (mMaxItemId == -1) { @@ -498,6 +514,30 @@ public class LauncherProvider extends ContentProvider { } } + /** + * Constructor used only in tests. + */ + public DatabaseHelper(Context context, String tableName) { + super(context, tableName, null, DATABASE_VERSION); + mContext = context; + + mAppWidgetHost = null; + mMaxItemId = initializeMaxItemId(getWritableDatabase()); + mMaxScreenId = initializeMaxScreenId(getWritableDatabase()); + } + + private boolean tableExists(String tableName) { + Cursor c = getReadableDatabase().query( + true, "sqlite_master", new String[] {"tbl_name"}, + "tbl_name = ?", new String[] {tableName}, + null, null, null, null, null); + try { + return c.getCount() > 0; + } finally { + c.close(); + } + } + public boolean wasNewDbCreated() { return mNewDbCreated; } @@ -510,37 +550,8 @@ public class LauncherProvider extends ContentProvider { mMaxScreenId = 0; mNewDbCreated = true; - UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); - long userSerialNumber = userManager.getSerialNumberForUser( - UserHandleCompat.myUserHandle()); - - db.execSQL("CREATE TABLE favorites (" + - "_id INTEGER PRIMARY KEY," + - "title TEXT," + - "intent TEXT," + - "container INTEGER," + - "screen INTEGER," + - "cellX INTEGER," + - "cellY INTEGER," + - "spanX INTEGER," + - "spanY INTEGER," + - "itemType INTEGER," + - "appWidgetId INTEGER NOT NULL DEFAULT -1," + - "isShortcut INTEGER," + - "iconType INTEGER," + - "iconPackage TEXT," + - "iconResource TEXT," + - "icon BLOB," + - "uri TEXT," + - "displayMode INTEGER," + - "appWidgetProvider TEXT," + - "modified INTEGER NOT NULL DEFAULT 0," + - "restored INTEGER NOT NULL DEFAULT 0," + - "profileId INTEGER DEFAULT " + userSerialNumber + "," + - "rank INTEGER NOT NULL DEFAULT 0," + - "options INTEGER NOT NULL DEFAULT 0" + - ");"); - addWorkspacesTable(db); + addFavoritesTable(db, false); + addWorkspacesTable(db, false); // Database was just created, so wipe any previous widgets if (mAppWidgetHost != null) { @@ -565,14 +576,59 @@ public class LauncherProvider extends ContentProvider { // Fresh and clean launcher DB. mMaxItemId = initializeMaxItemId(db); - setFlagEmptyDbCreated(); - - // When a new DB is created, remove all previously stored managed profile information. - ManagedProfileHeuristic.processAllUsers(Collections.emptyList(), mContext); + onEmptyDbCreated(); } - private void addWorkspacesTable(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" + + /** + * Overriden in tests. + */ + protected void onEmptyDbCreated() { + // Set the flag for empty DB + Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); + + // When a new DB is created, remove all previously stored managed profile information. + ManagedProfileHeuristic.processAllUsers(Collections.emptyList(), + mContext); + } + + protected long getDefaultUserSerial() { + return UserManagerCompat.getInstance(mContext).getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + } + + private void addFavoritesTable(SQLiteDatabase db, boolean optional) { + String ifNotExists = optional ? " IF NOT EXISTS " : ""; + db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" + + "_id INTEGER PRIMARY KEY," + + "title TEXT," + + "intent TEXT," + + "container INTEGER," + + "screen INTEGER," + + "cellX INTEGER," + + "cellY INTEGER," + + "spanX INTEGER," + + "spanY INTEGER," + + "itemType INTEGER," + + "appWidgetId INTEGER NOT NULL DEFAULT -1," + + "isShortcut INTEGER," + + "iconType INTEGER," + + "iconPackage TEXT," + + "iconResource TEXT," + + "icon BLOB," + + "uri TEXT," + + "displayMode INTEGER," + + "appWidgetProvider TEXT," + + "modified INTEGER NOT NULL DEFAULT 0," + + "restored INTEGER NOT NULL DEFAULT 0," + + "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," + + "rank INTEGER NOT NULL DEFAULT 0," + + "options INTEGER NOT NULL DEFAULT 0" + + ");"); + } + + private void addWorkspacesTable(SQLiteDatabase db, boolean optional) { + String ifNotExists = optional ? " IF NOT EXISTS " : ""; + db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" + LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," + LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + @@ -611,15 +667,7 @@ public class LauncherProvider extends ContentProvider { } private void setFlagJustLoadedOldDb() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); - } - - private void setFlagEmptyDbCreated() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); + Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); } @Override @@ -632,7 +680,7 @@ public class LauncherProvider extends ContentProvider { // With the new shrink-wrapped and re-orderable workspaces, it makes sense // to persist workspace screens and their relative order. mMaxScreenId = 0; - addWorkspacesTable(db); + addWorkspacesTable(db, false); } case 13: { db.beginTransaction(); @@ -678,7 +726,7 @@ public class LauncherProvider extends ContentProvider { // available (tablet users). Because one of the possible cling flows (migration) // is very destructive (wipes out workspaces), we want to prevent this from showing // until clear data. We do so by marking that the clings have been shown. - LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); + LauncherClings.markFirstRunClingDismissed(mContext); } case 17: { // No-op @@ -828,7 +876,7 @@ public class LauncherProvider extends ContentProvider { } db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); - addWorkspacesTable(db); + addWorkspacesTable(db, false); // Add all screen ids back int total = sortedIDs.size(); @@ -926,10 +974,6 @@ public class LauncherProvider extends ContentProvider { return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); } - public void updateMaxItemId(long id) { - mMaxItemId = id + 1; - } - public void checkId(String table, ContentValues values) { long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID); if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) { diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java index e8c11c48bd..55a512fb56 100644 --- a/src/com/android/launcher3/LauncherRootView.java +++ b/src/com/android/launcher3/LauncherRootView.java @@ -1,17 +1,71 @@ package com.android.launcher3; +import android.annotation.TargetApi; +import android.app.ActivityManager; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; +import android.view.View; public class LauncherRootView extends InsettableFrameLayout { + + private final Paint mOpaquePaint; + private boolean mDrawRightInsetBar; + private int mRightInsetBarWidth; + + private View mAlignedView; + public LauncherRootView(Context context, AttributeSet attrs) { super(context, attrs); + + mOpaquePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mOpaquePaint.setColor(Color.BLACK); + mOpaquePaint.setStyle(Paint.Style.FILL); } + @Override + protected void onFinishInflate() { + if (getChildCount() > 0) { + // LauncherRootView contains only one child, which should be aligned + // based on the horizontal insets. + mAlignedView = getChildAt(0); + } + super.onFinishInflate(); + } + + @TargetApi(23) @Override protected boolean fitSystemWindows(Rect insets) { - setInsets(insets); + mDrawRightInsetBar = insets.right > 0 && + (!Utilities.ATLEAST_MARSHMALLOW || + getContext().getSystemService(ActivityManager.class).isLowRamDevice()); + mRightInsetBarWidth = insets.right; + setInsets(mDrawRightInsetBar ? new Rect(0, insets.top, 0, insets.bottom) : insets); + + if (mAlignedView != null && mDrawRightInsetBar) { + // Apply margins on aligned view to handle left/right insets. + MarginLayoutParams lp = (MarginLayoutParams) mAlignedView.getLayoutParams(); + if (lp.leftMargin != insets.left || lp.rightMargin != insets.right) { + lp.leftMargin = insets.left; + lp.rightMargin = insets.right; + mAlignedView.setLayoutParams(lp); + } + } + return true; // I'll take it from here } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + // If the right inset is opaque, draw a black rectangle to ensure that is stays opaque. + if (mDrawRightInsetBar) { + int width = getWidth(); + canvas.drawRect(width - mRightInsetBarWidth, 0, width, getHeight(), mOpaquePaint); + } + } } \ No newline at end of file diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 8a5804f349..55e6395ac4 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -113,7 +113,7 @@ public class LauncherSettings { /** * The content:// style URL for this table */ - static final Uri CONTENT_URI = Uri.parse("content://" + + public static final Uri CONTENT_URI = Uri.parse("content://" + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** @@ -324,5 +324,8 @@ public class LauncherSettings { public static final String EXTRA_VALUE = "value"; public static final String EXTRA_DEFAULT_VALUE = "default_value"; + + // Extra for set_boolean method to also notify the backup manager of the change. + public static final String NOTIFY_BACKUP = "notify_backup"; } } diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java index cdde8c13fa..b95e2b031b 100644 --- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -22,6 +22,7 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.res.Resources; import android.util.Log; @@ -79,13 +80,18 @@ import java.util.HashMap; */ public class LauncherStateTransitionAnimation { + private static final float FINAL_REVEAL_ALPHA_FOR_WIDGETS = 0.3f; + /** * Private callbacks made during transition setup. */ - static abstract class PrivateTransitionCallbacks { - float getMaterialRevealViewFinalAlpha(View revealView) { - return 0; + private static class PrivateTransitionCallbacks { + private final float materialRevealViewFinalAlpha; + + PrivateTransitionCallbacks(float revealAlpha) { + materialRevealViewFinalAlpha = revealAlpha; } + float getMaterialRevealViewStartFinalRadius() { return 0; } @@ -96,7 +102,7 @@ public class LauncherStateTransitionAnimation { void onTransitionComplete() {} } - public static final String TAG = "LauncherStateTransitionAnimation"; + public static final String TAG = "LSTAnimation"; // Flags to determine how to set the layers on views before the transition animation public static final int BUILD_LAYER = 0; @@ -120,11 +126,7 @@ public class LauncherStateTransitionAnimation { final boolean animated, final boolean startSearchAfterTransition) { final AllAppsContainerView toView = mLauncher.getAppsView(); final View buttonView = mLauncher.getAllAppsButton(); - PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { - @Override - public float getMaterialRevealViewFinalAlpha(View revealView) { - return 1f; - } + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) { @Override public float getMaterialRevealViewStartFinalRadius() { int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; @@ -151,8 +153,7 @@ public class LauncherStateTransitionAnimation { }; // Only animate the search bar if animating from spring loaded mode back to all apps mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState, - Workspace.State.NORMAL_HIDDEN, buttonView, toView, toView.getContentView(), - toView.getRevealView(), toView.getSearchBarView(), animated, cb); + Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, cb); } /** @@ -163,15 +164,9 @@ public class LauncherStateTransitionAnimation { final WidgetsContainerView toView = mLauncher.getWidgetsView(); final View buttonView = mLauncher.getWidgetsButton(); - PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { - @Override - public float getMaterialRevealViewFinalAlpha(View revealView) { - return 0.3f; - } - }; mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState, - Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, toView.getContentView(), - toView.getRevealView(), null, animated, cb); + Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, animated, + new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS)); } /** @@ -199,16 +194,15 @@ public class LauncherStateTransitionAnimation { * Creates and starts a new animation to a particular overlay view. */ @SuppressLint("NewApi") - private AnimatorSet startAnimationToOverlay(final Workspace.State fromWorkspaceState, - final Workspace.State toWorkspaceState, final View buttonView, final View toView, - final View contentView, final View revealView, final View overlaySearchBarView, + private AnimatorSet startAnimationToOverlay( + final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, + final View buttonView, final BaseContainerView toView, final boolean animated, final PrivateTransitionCallbacks pCb) { final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); final boolean material = Utilities.ATLEAST_LOLLIPOP; final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_overlayItemsAlphaStagger); + final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); final View fromView = mLauncher.getWorkspace(); @@ -226,11 +220,17 @@ public class LauncherStateTransitionAnimation { animated, layerViews); // Animate the search bar - startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState, - animated ? revealDuration : 0, overlaySearchBarView); + startWorkspaceSearchBarAnimation( + toWorkspaceState, animated ? revealDuration : 0, animation); + + Animator updateTransitionStepAnim = dispatchOnLauncherTransitionStepAnim(fromView, toView); + + final View contentView = toView.getContentView(); if (animated && initialized) { // Setup the reveal view animation + final View revealView = toView.getRevealView(); + int width = revealView.getMeasuredWidth(); int height = revealView.getMeasuredHeight(); float revealRadius = (float) Math.hypot(width / 2, height / 2); @@ -244,9 +244,9 @@ public class LauncherStateTransitionAnimation { final float revealViewToXDrift; final float revealViewToYDrift; if (material) { - int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, - buttonView, null); - revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView); + int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace( + revealView, buttonView, null); + revealViewToAlpha = pCb.materialRevealViewFinalAlpha; revealViewToYDrift = buttonViewToPanelDelta[1]; revealViewToXDrift = buttonViewToPanelDelta[0]; } else { @@ -271,15 +271,6 @@ public class LauncherStateTransitionAnimation { layerViews.put(revealView, BUILD_AND_SET_LAYER); animation.play(panelAlphaAndDrift); - if (overlaySearchBarView != null) { - overlaySearchBarView.setAlpha(0f); - ObjectAnimator searchBarAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 0f, 1f); - searchBarAlpha.setDuration(100); - searchBarAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); - animation.play(searchBarAlpha); - } - // Setup the animation for the content view contentView.setVisibility(View.VISIBLE); contentView.setAlpha(0f); @@ -342,11 +333,12 @@ public class LauncherStateTransitionAnimation { animation.play(workspaceAnim); } + animation.play(updateTransitionStepAnim); + // Dispatch the prepare transition signal dispatchOnLauncherTransitionPrepare(fromView, animated, false); dispatchOnLauncherTransitionPrepare(toView, animated, false); - final AnimatorSet stateAnimation = animation; final Runnable startAnimRunnable = new Runnable() { public void run() { @@ -401,6 +393,24 @@ public class LauncherStateTransitionAnimation { } } + /** + * Returns an Animator that calls {@link #dispatchOnLauncherTransitionStep(View, float)} on + * {@param fromView} and {@param toView} as the animation interpolates. + * + * This is a bit hacky: we create a dummy ValueAnimator just for the AnimatorUpdateListener. + */ + private Animator dispatchOnLauncherTransitionStepAnim(final View fromView, final View toView) { + ValueAnimator updateAnimator = ValueAnimator.ofFloat(0, 1); + updateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + dispatchOnLauncherTransitionStep(fromView, animation.getAnimatedFraction()); + dispatchOnLauncherTransitionStep(toView, animation.getAnimatedFraction()); + } + }); + return updateAnimator; + } + /** * Starts and animation to the workspace from the apps view. */ @@ -408,12 +418,8 @@ public class LauncherStateTransitionAnimation { final Workspace.State toWorkspaceState, final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { AllAppsContainerView appsView = mLauncher.getAppsView(); - PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { - @Override - float getMaterialRevealViewFinalAlpha(View revealView) { - // No alpha anim from all apps - return 1f; - } + // No alpha anim from all apps + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) { @Override float getMaterialRevealViewStartFinalRadius() { int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; @@ -441,9 +447,8 @@ public class LauncherStateTransitionAnimation { }; // Only animate the search bar if animating to spring loaded mode from all apps mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState, - toWorkspacePage, mLauncher.getAllAppsButton(), appsView, appsView.getContentView(), - appsView.getRevealView(), appsView.getSearchBarView(), animated, - onCompleteRunnable, cb); + toWorkspacePage, mLauncher.getAllAppsButton(), appsView, + animated, onCompleteRunnable, cb); } /** @@ -453,11 +458,8 @@ public class LauncherStateTransitionAnimation { final Workspace.State toWorkspaceState, final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); - PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { - @Override - float getMaterialRevealViewFinalAlpha(View revealView) { - return 0.3f; - } + PrivateTransitionCallbacks cb = + new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS) { @Override public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( final View revealView, final View widgetsButtonView) { @@ -469,19 +471,20 @@ public class LauncherStateTransitionAnimation { }; } }; - mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, - toWorkspaceState, toWorkspacePage, mLauncher.getWidgetsButton(), widgetsView, - widgetsView.getContentView(), widgetsView.getRevealView(), null, animated, - onCompleteRunnable, cb); + mCurrentAnimation = startAnimationToWorkspaceFromOverlay( + fromWorkspaceState, toWorkspaceState, + toWorkspacePage, mLauncher.getWidgetsButton(), widgetsView, + animated, onCompleteRunnable, cb); } /** * Creates and starts a new animation to the workspace. */ - private AnimatorSet startAnimationToWorkspaceFromOverlay(final Workspace.State fromWorkspaceState, - final Workspace.State toWorkspaceState, final int toWorkspacePage, final View buttonView, - final View fromView, final View contentView, final View revealView, - final View overlaySearchBarView, final boolean animated, final Runnable onCompleteRunnable, + private AnimatorSet startAnimationToWorkspaceFromOverlay( + final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, + final int toWorkspacePage, + final View buttonView, final BaseContainerView fromView, + final boolean animated, final Runnable onCompleteRunnable, final PrivateTransitionCallbacks pCb) { final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); @@ -506,8 +509,10 @@ public class LauncherStateTransitionAnimation { toWorkspacePage, animated, layerViews); // Animate the search bar - startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState, - animated ? revealDuration : 0, overlaySearchBarView); + startWorkspaceSearchBarAnimation( + toWorkspaceState, animated ? revealDuration : 0, animation); + + Animator updateTransitionStepAnim = dispatchOnLauncherTransitionStepAnim(fromView, toView); if (animated && initialized) { // Play the workspace animation @@ -515,6 +520,10 @@ public class LauncherStateTransitionAnimation { animation.play(workspaceAnim); } + animation.play(updateTransitionStepAnim); + final View revealView = fromView.getRevealView(); + final View contentView = fromView.getContentView(); + // hideAppsCustomizeHelper is called in some cases when it is already hidden // don't perform all these no-op animations. In particularly, this was causing // the all-apps button to pop in and out. @@ -562,7 +571,7 @@ public class LauncherStateTransitionAnimation { // Setup animation for the reveal panel alpha final float revealViewToAlpha = !material ? 0f : - pCb.getMaterialRevealViewFinalAlpha(revealView); + pCb.materialRevealViewFinalAlpha; if (revealViewToAlpha != 1f) { ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha", 1f, revealViewToAlpha); @@ -590,16 +599,6 @@ public class LauncherStateTransitionAnimation { itemsAlpha.setInterpolator(decelerateInterpolator); animation.play(itemsAlpha); - if (overlaySearchBarView != null) { - overlaySearchBarView.setAlpha(1f); - ObjectAnimator searchAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 1f, 0f); - searchAlpha.setDuration(material ? 100 : 150); - searchAlpha.setInterpolator(decelerateInterpolator); - searchAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); - layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); - animation.play(searchAlpha); - } - if (material) { // Animate the all apps button float finalRadius = pCb.getMaterialRevealViewStartFinalRadius(); @@ -615,11 +614,11 @@ public class LauncherStateTransitionAnimation { } animation.play(reveal); } - - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); } + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -645,9 +644,6 @@ public class LauncherStateTransitionAnimation { contentView.setTranslationY(0); contentView.setAlpha(1); } - if (overlaySearchBarView != null) { - overlaySearchBarView.setAlpha(1f); - } // This can hold unnecessary references to views. cleanupAnimation(); @@ -703,35 +699,11 @@ public class LauncherStateTransitionAnimation { /** * Coordinates the workspace search bar animation along with the launcher state animation. */ - private void startWorkspaceSearchBarAnimation(AnimatorSet animation, - final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, int duration, - View overlaySearchBar) { + private void startWorkspaceSearchBarAnimation( + final Workspace.State toWorkspaceState, int duration, AnimatorSet animation) { final SearchDropTargetBar.State toSearchBarState = - toWorkspaceState.getSearchDropTargetBarState(); - - if (overlaySearchBar != null) { - if ((toWorkspaceState == Workspace.State.NORMAL) && - (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN)) { - // If we are transitioning from the overlay to the workspace, then show the - // workspace search bar immediately and let the overlay search bar fade out on top - mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0); - } else if (fromWorkspaceState == Workspace.State.NORMAL) { - // If we are transitioning from the workspace to the overlay, then keep the - // workspace search bar visible until the overlay search bar fades in on top - animation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0); - } - }); - } else { - // Otherwise, then just animate the workspace search bar normally - mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration); - } - } else { - // If there is no overlay search bar, then just animate the workspace search bar - mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration); - } + toWorkspaceState.searchDropTargetBarState; + mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration, animation); } /** diff --git a/src/com/android/launcher3/PageIndicatorMarker.java b/src/com/android/launcher3/PageIndicatorMarker.java index f012db74b0..7bf21dd905 100644 --- a/src/com/android/launcher3/PageIndicatorMarker.java +++ b/src/com/android/launcher3/PageIndicatorMarker.java @@ -45,6 +45,7 @@ public class PageIndicatorMarker extends FrameLayout { } protected void onFinishInflate() { + super.onFinishInflate(); mActiveMarker = (ImageView) findViewById(R.id.active); mInactiveMarker = (ImageView) findViewById(R.id.inactive); } diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 05f0a05534..2dcff35b5e 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -28,6 +28,7 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.os.Parcel; @@ -87,8 +88,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private int mFreeScrollMinScrollX = -1; private int mFreeScrollMaxScrollX = -1; - static final int AUTOMATIC_PAGE_SPACING = -1; - protected int mFlingThresholdVelocity; protected int mMinFlingVelocity; protected int mMinSnapVelocity; @@ -133,8 +132,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected final static int TOUCH_STATE_NEXT_PAGE = 3; protected final static int TOUCH_STATE_REORDERING = 4; - protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; - protected int mTouchState = TOUCH_STATE_REST; protected boolean mForceScreenScrolled = false; @@ -144,12 +141,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private int mMaximumVelocity; protected int mPageLayoutWidthGap; protected int mPageLayoutHeightGap; - protected int mCellCountX = 0; - protected int mCellCountY = 0; protected boolean mCenterPagesVertically; protected boolean mAllowOverScroll = true; protected int[] mTempVisiblePagesRange = new int[2]; - protected boolean mForceDrawAllChildrenNextFrame; protected static final int INVALID_POINTER = -1; @@ -198,6 +192,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private static final float[] sTmpPoint = new float[2]; private static final int[] sTmpIntPoint = new int[2]; private static final Rect sTmpRect = new Rect(); + private static final RectF sTmpRectF = new RectF(); protected final Rect mInsets = new Rect(); protected final boolean mIsRtl; @@ -819,7 +814,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc childWidthMode = MeasureSpec.EXACTLY; childHeightMode = MeasureSpec.EXACTLY; - childWidth = getViewportWidth() - mInsets.left - mInsets.right; + childWidth = getViewportWidth(); childHeight = getViewportHeight(); } if (referenceChildWidth == 0) { @@ -1057,22 +1052,26 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected void getVisiblePages(int[] range) { final int pageCount = getChildCount(); - sTmpIntPoint[0] = sTmpIntPoint[1] = 0; - range[0] = -1; range[1] = -1; if (pageCount > 0) { - int viewportWidth = getViewportWidth(); + final int visibleLeft = -getLeft(); + final int visibleRight = visibleLeft + getViewportWidth(); int curScreen = 0; int count = getChildCount(); for (int i = 0; i < count; i++) { View currPage = getPageAt(i); - sTmpIntPoint[0] = 0; - Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); - if (sTmpIntPoint[0] > viewportWidth) { + // Verify if the page bounds are within the visible range. + sTmpRectF.left = 0; + sTmpRectF.right = currPage.getMeasuredWidth(); + currPage.getMatrix().mapRect(sTmpRectF); + sTmpRectF.offset(currPage.getLeft() - getScrollX(), 0); + getMatrix().mapRect(sTmpRectF); + + if (sTmpRectF.left > visibleRight || sTmpRectF.right < visibleLeft) { if (range[0] == -1) { continue; } else { @@ -1080,15 +1079,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - sTmpIntPoint[0] = currPage.getMeasuredWidth(); - Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); - if (sTmpIntPoint[0] < 0) { - if (range[0] == -1) { - continue; - } else { - break; - } - } curScreen = i; if (range[0] < 0) { range[0] = curScreen; @@ -1136,8 +1126,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc for (int i = pageCount - 1; i >= 0; i--) { final View v = getPageAt(i); if (v == mDragView) continue; - if (mForceDrawAllChildrenNextFrame || - (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { + if (leftScreen <= i && i <= rightScreen && shouldDrawChild(v)) { drawChild(canvas, v, drawingTime); } } @@ -1146,7 +1135,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc drawChild(canvas, mDragView, drawingTime); } - mForceDrawAllChildrenNextFrame = false; canvas.restore(); } } @@ -1177,6 +1165,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc canvas.rotate(90); getEdgeVerticalPostion(sTmpIntPoint); + canvas.translate(sTmpIntPoint[0] - display.top, -display.width()); mEdgeGlowRight.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); if (mEdgeGlowRight.draw(canvas)) { @@ -1219,7 +1208,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public boolean dispatchUnhandledMove(View focused, int direction) { - // XXX-RTL: This will be fixed in a future CL + if (super.dispatchUnhandledMove(focused, direction)) { + return true; + } + + if (mIsRtl) { + if (direction == View.FOCUS_LEFT) { + direction = View.FOCUS_RIGHT; + } else if (direction == View.FOCUS_RIGHT) { + direction = View.FOCUS_LEFT; + } + } if (direction == View.FOCUS_LEFT) { if (getCurrentPage() > 0) { snapToPage(getCurrentPage() - 1); @@ -1231,7 +1230,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return true; } } - return super.dispatchUnhandledMove(focused, direction); + return false; } @Override @@ -2072,11 +2071,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc whichPage = validateNewPage(whichPage); mNextPage = whichPage; - View focusedChild = getFocusedChild(); - if (focusedChild != null && whichPage != mCurrentPage && - focusedChild == getPageAt(mCurrentPage)) { - focusedChild.clearFocus(); - } pageBeginMoving(); awakenScrollBars(duration); diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java index 40eadabd5c..1c0290417f 100644 --- a/src/com/android/launcher3/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -133,7 +133,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen // 3) Setup icon in the center and app icon in the top right corner. if (mDisabledForSafeMode) { FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); - disabledIcon.setGhostModeEnabled(true); + disabledIcon.setState(FastBitmapDrawable.State.DISABLED); mCenterDrawable = disabledIcon; mSettingIconDrawable = null; } else if (isReadyForClickSetup()) { @@ -218,7 +218,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); mCenterDrawable.setBounds(mRect); } else { - float iconSize = Math.min(availableWidth, availableHeight); + float iconSize = 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. @@ -231,26 +231,30 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx); - // Recreate the setup text. - mSetupTextLayout = new StaticLayout( - getResources().getText(R.string.gadget_setup_text), mPaint, availableWidth, - Layout.Alignment.ALIGN_CENTER, 1, 0, true); - int textHeight = mSetupTextLayout.getHeight(); + // Icon top when we do not draw the text + int iconTop = (getHeight() - actualIconSize) / 2; + mSetupTextLayout = null; - // Extra icon size due to the setting icon - float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor - + grid.iconDrawablePaddingPx; + if (availableWidth > 0) { + // Recreate the setup text. + mSetupTextLayout = new StaticLayout( + getResources().getText(R.string.gadget_setup_text), mPaint, availableWidth, + Layout.Alignment.ALIGN_CENTER, 1, 0, true); + int textHeight = mSetupTextLayout.getHeight(); - int iconTop; - if (minHeightWithText < availableHeight) { - // We can draw the text as well - iconTop = (getHeight() - textHeight - - grid.iconDrawablePaddingPx - actualIconSize) / 2; + // Extra icon size due to the setting icon + float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor + + grid.iconDrawablePaddingPx; - } else { - // The text will not fit. Only draw the icons. - iconTop = (getHeight() - actualIconSize) / 2; - mSetupTextLayout = null; + 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); diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index 45e4b2c4ad..908c8b9e28 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -179,7 +179,8 @@ class PreloadIconDrawable extends Drawable { mPaint.setColor(getIndicatorColor()); } if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); + ((FastBitmapDrawable) mIcon).setState(level <= 0 ? + FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL); } invalidateSelf(); diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 772a334b9e..f7288a0a06 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -18,12 +18,18 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityManager; +import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import com.android.launcher3.util.Thunk; @@ -34,35 +40,32 @@ import com.android.launcher3.util.Thunk; */ public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { + private static final TimeInterpolator MOVE_DOWN_INTERPOLATOR = new DecelerateInterpolator(0.6f); + private static final TimeInterpolator MOVE_UP_INTERPOLATOR = new DecelerateInterpolator(1.5f); + private static final TimeInterpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(); + /** The different states that the search bar space can be in. */ public enum State { - INVISIBLE (0f, 0f), - SEARCH_BAR (1f, 0f), - DROP_TARGET (0f, 1f); + INVISIBLE (0f, 0f, 0f), + INVISIBLE_TRANSLATED (0f, 0f, -1f), + SEARCH_BAR (1f, 0f, 0f), + DROP_TARGET (0f, 1f, 0f); private final float mSearchBarAlpha; private final float mDropTargetBarAlpha; + private final float mTranslation; - State(float sbAlpha, float dtbAlpha) { + State(float sbAlpha, float dtbAlpha, float translation) { mSearchBarAlpha = sbAlpha; mDropTargetBarAlpha = dtbAlpha; + mTranslation = translation; } - float getSearchBarAlpha() { - return mSearchBarAlpha; - } - - float getDropTargetBarAlpha() { - return mDropTargetBarAlpha; - } } private static int DEFAULT_DRAG_FADE_DURATION = 175; - private LauncherViewPropertyAnimator mDropTargetBarAnimator; - private LauncherViewPropertyAnimator mQSBSearchBarAnimator; - private static final AccelerateInterpolator sAccelerateInterpolator = - new AccelerateInterpolator(); + private AnimatorSet mCurrentAnimation; private State mState = State.SEARCH_BAR; @Thunk View mQSB; @@ -116,49 +119,11 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D // Create the various fade animations mDropTargetBar.setAlpha(0f); - mDropTargetBarAnimator = new LauncherViewPropertyAnimator(mDropTargetBar); - mDropTargetBarAnimator.setInterpolator(sAccelerateInterpolator); - mDropTargetBarAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - // Ensure that the view is visible for the animation - mDropTargetBar.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mDropTargetBar != null) { - AlphaUpdateListener.updateVisibility(mDropTargetBar, mAccessibilityEnabled); - } - } - }); + AlphaUpdateListener.updateVisibility(mDropTargetBar, mAccessibilityEnabled); } public void setQsbSearchBar(View qsb) { mQSB = qsb; - if (mQSB != null) { - // Update the search ber animation - mQSBSearchBarAnimator = new LauncherViewPropertyAnimator(mQSB); - mQSBSearchBarAnimator.setInterpolator(sAccelerateInterpolator); - mQSBSearchBarAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - // Ensure that the view is visible for the animation - if (mQSB != null) { - mQSB.setVisibility(View.VISIBLE); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mQSB != null) { - AlphaUpdateListener.updateVisibility(mQSB, mAccessibilityEnabled); - } - } - }); - } else { - mQSBSearchBarAnimator = null; - } } /** @@ -166,6 +131,10 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D * the state is applied immediately. */ public void animateToState(State newState, int duration) { + animateToState(newState, duration, null); + } + + public void animateToState(State newState, int duration, AnimatorSet animation) { if (mState != newState) { mState = newState; @@ -174,30 +143,63 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); mAccessibilityEnabled = am.isEnabled(); - animateViewAlpha(mQSBSearchBarAnimator, mQSB, newState.getSearchBarAlpha(), - duration); - animateViewAlpha(mDropTargetBarAnimator, mDropTargetBar, newState.getDropTargetBarAlpha(), - duration); + if (mCurrentAnimation != null) { + mCurrentAnimation.cancel(); + mCurrentAnimation = null; + } + mCurrentAnimation = null; + + if (duration > 0) { + mCurrentAnimation = new AnimatorSet(); + mCurrentAnimation.setDuration(duration); + + animateAlpha(mDropTargetBar, mState.mDropTargetBarAlpha, DEFAULT_INTERPOLATOR); + } else { + mDropTargetBar.setAlpha(mState.mDropTargetBarAlpha); + AlphaUpdateListener.updateVisibility(mDropTargetBar, mAccessibilityEnabled); + } + + if (mQSB != null) { + boolean isVertical = ((Launcher) getContext()).getDeviceProfile() + .isVerticalBarLayout(); + float translation = isVertical ? 0 : mState.mTranslation * getMeasuredHeight(); + + if (duration > 0) { + int translationChange = Float.compare(mQSB.getTranslationY(), translation); + + animateAlpha(mQSB, mState.mSearchBarAlpha, + translationChange == 0 ? DEFAULT_INTERPOLATOR + : (translationChange < 0 ? MOVE_DOWN_INTERPOLATOR + : MOVE_UP_INTERPOLATOR)); + + if (translationChange != 0) { + mCurrentAnimation.play( + ObjectAnimator.ofFloat(mQSB, View.TRANSLATION_Y, translation)); + } + } else { + mQSB.setTranslationY(translation); + mQSB.setAlpha(mState.mSearchBarAlpha); + AlphaUpdateListener.updateVisibility(mQSB, mAccessibilityEnabled); + } + } + + // Start the final animation + if (duration > 0) { + if (animation != null) { + animation.play(mCurrentAnimation); + } else { + mCurrentAnimation.start(); + } + } } } - /** - * Convenience method to animate the alpha of a view using hardware layers. - */ - private void animateViewAlpha(LauncherViewPropertyAnimator animator, View v, float alpha, - int duration) { - if (v == null) { - return; - } - - animator.cancel(); + private void animateAlpha(View v, float alpha, TimeInterpolator interpolator) { if (Float.compare(v.getAlpha(), alpha) != 0) { - if (duration > 0) { - animator.alpha(alpha).withLayer().setDuration(duration).start(); - } else { - v.setAlpha(alpha); - AlphaUpdateListener.updateVisibility(v, mAccessibilityEnabled); - } + ObjectAnimator anim = ObjectAnimator.ofFloat(v, View.ALPHA, alpha); + anim.setInterpolator(interpolator); + anim.addListener(new ViewVisiblilyUpdateHandler(v)); + mCurrentAnimation.play(anim); } } @@ -253,4 +255,24 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D mDeleteDropTarget.enableAccessibleDrag(enable); mUninstallDropTarget.enableAccessibleDrag(enable); } + + private class ViewVisiblilyUpdateHandler extends AnimatorListenerAdapter { + private final View mView; + + ViewVisiblilyUpdateHandler(View v) { + mView = v; + } + + @Override + public void onAnimationStart(Animator animation) { + // Ensure that the view is visible for the animation + mView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation){ + AlphaUpdateListener.updateVisibility(mView, mAccessibilityEnabled); + } + + } } diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index 157b48a397..21e72e99a4 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -23,7 +23,6 @@ import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; public class ShortcutAndWidgetContainer extends ViewGroup { static final String TAG = "CellLayoutChildren"; @@ -164,8 +163,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup { lp.height = getMeasuredHeight(); } int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, - MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childheightMeasureSpec); } diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 5766cf2f20..60e080ea64 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -52,7 +52,7 @@ public class ShortcutInfo extends ItemInfo { public static final int FLAG_AUTOINTALL_ICON = 2; //0B10; /** - * The icon is being installed. If {@link FLAG_RESTORED_ICON} or {@link FLAG_AUTOINTALL_ICON} + * The icon is being installed. If {@link #FLAG_RESTORED_ICON} or {@link #FLAG_AUTOINTALL_ICON} * is set, then the icon is either being installed or is in a broken state. */ public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; // 0B100; @@ -112,6 +112,16 @@ public class ShortcutInfo extends ItemInfo { */ public static final int FLAG_DISABLED_NOT_AVAILABLE = 2; + /** + * Indicates that the icon is disabled as the app is suspended + */ + public static final int FLAG_DISABLED_SUSPENDED = 4; + + /** + * Indicates that the icon is disabled as the user is in quiet mode. + */ + public static final int FLAG_DISABLED_QUIET_USER = 8; + /** * Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when * sd-card is not available). @@ -126,19 +136,14 @@ public class ShortcutInfo extends ItemInfo { private int mInstallProgress; /** - * Refer {@link AppInfo#firstInstallTime}. - */ - public long firstInstallTime; - - /** - * TODO move this to {@link status} + * TODO move this to {@link #status} */ int flags = 0; /** * If this shortcut is a placeholder, then intent will be a market intent for the package, and * this will hold the original intent from the database. Otherwise, null. - * Refer {@link #FLAG_RESTORE_PENDING}, {@link #FLAG_INSTALL_PENDING} + * Refer {@link #FLAG_RESTORED_ICON}, {@link #FLAG_AUTOINTALL_ICON} */ Intent promisedIntent; @@ -172,7 +177,6 @@ public class ShortcutInfo extends ItemInfo { mIcon = info.mIcon; // TODO: should make a copy here. maybe we don't need this ctor at all customIcon = info.customIcon; flags = info.flags; - firstInstallTime = info.firstInstallTime; user = info.user; status = info.status; } @@ -184,7 +188,7 @@ public class ShortcutInfo extends ItemInfo { intent = new Intent(info.intent); customIcon = false; flags = info.flags; - firstInstallTime = info.firstInstallTime; + isDisabled = info.isDisabled; } public void setIcon(Bitmap b) { @@ -293,8 +297,12 @@ public class ShortcutInfo extends ItemInfo { shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser()); shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; shortcut.flags = AppInfo.initFlags(info); - shortcut.firstInstallTime = info.getFirstInstallTime(); return shortcut; } + + @Override + public boolean isDisabled() { + return isDisabled != 0; + } } diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index cb0e252b2e..d46c939c9b 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -31,7 +31,7 @@ public class Stats { * Implemented by containers to provide a launch source for a given child. */ public interface LaunchSourceProvider { - void fillInLaunchSourceData(Bundle sourceData); + void fillInLaunchSourceData(View v, Bundle sourceData); } /** @@ -70,7 +70,7 @@ public class Stats { } if (provider != null) { - provider.fillInLaunchSourceData(sourceData); + provider.fillInLaunchSourceData(v, sourceData); } else if (LauncherAppState.isDogfoodBuild()) { throw new RuntimeException("Expected LaunchSourceProvider"); } @@ -132,6 +132,7 @@ public class Stats { final String flat = intent.toUri(0); Intent broadcastIntent = new Intent(ACTION_LAUNCH).putExtra(EXTRA_INTENT, flat); + if (shortcut != null) { broadcastIntent.putExtra(EXTRA_CONTAINER, shortcut.container) .putExtra(EXTRA_SCREEN, shortcut.screenId) @@ -142,6 +143,11 @@ public class Stats { Bundle sourceExtras = LaunchSourceUtils.createSourceData(); LaunchSourceUtils.populateSourceDataFromAncestorProvider(v, sourceExtras); broadcastIntent.putExtra(EXTRA_SOURCE, sourceExtras); - mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); + + String[] packages = mLauncher.getResources().getStringArray(R.array.launch_broadcast_targets); + for(String p: packages) { + broadcastIntent.setPackage(p); + mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); + } } } diff --git a/src/com/android/launcher3/ToggleWeightWatcher.java b/src/com/android/launcher3/ToggleWeightWatcher.java deleted file mode 100644 index 33701a2c43..0000000000 --- a/src/com/android/launcher3/ToggleWeightWatcher.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.launcher3; - -import android.app.Activity; - -public class ToggleWeightWatcher extends Activity { - -} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index adedd33b2f..271e581f01 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -46,8 +46,12 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.os.Build; import android.os.Bundle; +import android.os.PowerManager; import android.os.Process; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.TtsSpan; import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; @@ -56,11 +60,19 @@ import android.util.TypedValue; import android.view.View; import android.widget.Toast; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.IconNormalizer; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -105,10 +117,29 @@ public final class Utilities { public static final boolean ATLEAST_JB_MR2 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; - // To turn on these properties, type - // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] - private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; - private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + public static boolean isNycOrAbove() { + // TODO: Replace using reflection with looking at the API version once + // Build.VERSION.SDK_INT gets bumped to 24. b/22942492. + try { + View.class.getDeclaredField("DRAG_FLAG_OPAQUE"); + // View.DRAG_FLAG_OPAQUE doesn't exist in M-release, so it's an indication of N+. + return true; + } catch (NoSuchFieldException e) { + return false; + } + } + + // These values are same as that in {@link AsyncTask}. + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE = 1; + /** + * An {@link Executor} to be used with async task with no limit on the queue size. + */ + public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, new LinkedBlockingQueue()); public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; @@ -116,16 +147,24 @@ public final class Utilities { return Log.isLoggable(propertyName, Log.VERBOSE); } - public static boolean isAllowRotationPrefEnabled(Context context, boolean multiProcess) { - SharedPreferences sharedPrefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE | (multiProcess ? - Context.MODE_MULTI_PROCESS : 0)); - boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false); - return sForceEnableRotation || allowRotationPref; - } - - public static boolean isRotationAllowedForDevice(Context context) { - return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); + public static boolean isAllowRotationPrefEnabled(Context context) { + boolean allowRotationPref = false; + if (isNycOrAbove()) { + // If the device was scaled, used the original dimensions to determine if rotation + // is allowed of not. + try { + // TODO: Use the actual field when the API is finalized. + int originalDensity = + DisplayMetrics.class.getField("DENSITY_DEVICE_STABLE").getInt(null); + Resources res = context.getResources(); + int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp + * res.getDisplayMetrics().densityDpi / originalDensity; + allowRotationPref = originalSmallestWidth >= 600; + } catch (Exception e) { + // Ignore + } + } + return getPrefs(context).getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, allowRotationPref); } public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { @@ -174,10 +213,42 @@ public final class Utilities { return createIconBitmap(new BitmapDrawable(context.getResources(), icon), context); } + /** + * Returns a bitmap suitable for the all apps view. The icon is badged for {@param user}. + * The bitmap is also visually normalized with other icons. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static Bitmap createBadgedIconBitmap( + Drawable icon, UserHandleCompat user, Context context) { + float scale = FeatureFlags.LAUNCHER3_ICON_NORMALIZATION ? + IconNormalizer.getInstance().getScale(icon) : 1; + Bitmap bitmap = createIconBitmap(icon, context, scale); + if (Utilities.ATLEAST_LOLLIPOP && user != null + && !UserHandleCompat.myUserHandle().equals(user)) { + BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); + Drawable badged = context.getPackageManager().getUserBadgedIcon( + drawable, user.getUser()); + if (badged instanceof BitmapDrawable) { + return ((BitmapDrawable) badged).getBitmap(); + } else { + return createIconBitmap(badged, context); + } + } else { + return bitmap; + } + } + /** * Returns a bitmap suitable for the all apps view. */ public static Bitmap createIconBitmap(Drawable icon, Context context) { + return createIconBitmap(icon, context, 1.0f /* scale */); + } + + /** + * @param scale the scale to apply before drawing {@param icon} on the canvas + */ + public static Bitmap createIconBitmap(Drawable icon, Context context, float scale) { synchronized (sCanvas) { final int iconBitmapSize = getIconBitmapSize(); @@ -192,7 +263,7 @@ public final class Utilities { // Ensure the bitmap has a density. BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; Bitmap bitmap = bitmapDrawable.getBitmap(); - if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { + if (bitmap != null && bitmap.getDensity() == Bitmap.DENSITY_NONE) { bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics()); } } @@ -233,7 +304,10 @@ public final class Utilities { sOldBounds.set(icon.getBounds()); icon.setBounds(left, top, left+width, top+height); + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(scale, scale, textureWidth / 2, textureHeight / 2); icon.draw(canvas); + canvas.restore(); icon.setBounds(sOldBounds); canvas.setBitmap(null); @@ -290,7 +364,7 @@ public final class Utilities { } /** - * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}. + * Inverse of {@link #getDescendantCoordRelativeToParent(View, View, int[], boolean)}. */ public static float mapCoordInSelfToDescendent(View descendant, View root, int[] coord) { @@ -710,4 +784,62 @@ public final class Utilities { public static String createDbSelectionQuery(String columnName, Iterable values) { return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); } + + /** + * Wraps a message with a TTS span, so that a different message is spoken than + * what is getting displayed. + * @param msg original message + * @param ttsMsg message to be spoken + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) { + if (Utilities.ATLEAST_LOLLIPOP) { + SpannableString spanned = new SpannableString(msg); + spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(), + 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + return spanned; + } else { + return msg; + } + } + + /** + * Replacement for Long.compare() which was added in API level 19. + */ + public static int longCompare(long lhs, long rhs) { + return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); + } + + public static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences( + LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean isPowerSaverOn(Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return ATLEAST_LOLLIPOP && powerManager.isPowerSaveMode(); + } + + /** + * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. + * This allows the badging to be done based on the action bitmap size rather than + * the scaled bitmap size. + */ + private static class FixedSizeBitmapDrawable extends BitmapDrawable { + + public FixedSizeBitmapDrawable(Bitmap bitmap) { + super(null, bitmap); + } + + @Override + public int getIntrinsicHeight() { + return getBitmap().getWidth(); + } + + @Override + public int getIntrinsicWidth() { + return getBitmap().getWidth(); + } + } } diff --git a/src/com/android/launcher3/WallpaperChangedReceiver.java b/src/com/android/launcher3/WallpaperChangedReceiver.java index 2d5612f120..c24fbffc6d 100644 --- a/src/com/android/launcher3/WallpaperChangedReceiver.java +++ b/src/com/android/launcher3/WallpaperChangedReceiver.java @@ -22,8 +22,6 @@ import android.content.Intent; public class WallpaperChangedReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent data) { - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState appState = LauncherAppState.getInstance(); - appState.onWallpaperChanged(); + LauncherAppState.getInstance().onWallpaperChanged(); } } diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 346055566c..314dd8a60d 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -10,7 +10,6 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; @@ -32,6 +31,7 @@ import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.SQLiteCacheHelper; import com.android.launcher3.util.Thunk; import com.android.launcher3.widget.WidgetCell; @@ -65,7 +65,7 @@ public class WidgetPreviewLoader { private final Context mContext; private final IconCache mIconCache; private final UserManagerCompat mUserManager; - private final AppWidgetManagerCompat mManager; + private final AppWidgetManagerCompat mWidgetManager; private final CacheDb mDb; private final int mProfileBadgeMargin; @@ -75,7 +75,7 @@ public class WidgetPreviewLoader { public WidgetPreviewLoader(Context context, IconCache iconCache) { mContext = context; mIconCache = iconCache; - mManager = AppWidgetManagerCompat.getInstance(context); + mWidgetManager = AppWidgetManagerCompat.getInstance(context); mUserManager = UserManagerCompat.getInstance(context); mDb = new CacheDb(context); mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); @@ -96,7 +96,7 @@ public class WidgetPreviewLoader { WidgetCacheKey key = getObjectKey(o, size); PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller); - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); return new PreviewLoadRequest(task); } @@ -104,7 +104,7 @@ public class WidgetPreviewLoader { * The DB holds the generated previews for various components. Previews can also have different * sizes (landscape vs portrait). */ - private static class CacheDb extends SQLiteOpenHelper { + private static class CacheDb extends SQLiteCacheHelper { private static final int DB_VERSION = 4; private static final String TABLE_NAME = "shortcut_and_widget_previews"; @@ -117,11 +117,11 @@ public class WidgetPreviewLoader { private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; public CacheDb(Context context) { - super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION); + super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME); } @Override - public void onCreate(SQLiteDatabase database) { + public void onCreateTable(SQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + COLUMN_COMPONENT + " TEXT NOT NULL, " + COLUMN_USER + " INTEGER NOT NULL, " + @@ -133,32 +133,13 @@ public class WidgetPreviewLoader { "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + ");"); } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion != newVersion) { - clearDB(db); - } - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion != newVersion) { - clearDB(db); - } - } - - private void clearDB(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } } private WidgetCacheKey getObjectKey(Object o, String size) { // should cache the string builder if (o instanceof LauncherAppWidgetProviderInfo) { LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o; - return new WidgetCacheKey(info.provider, mManager.getUser(info), size); + return new WidgetCacheKey(info.provider, mWidgetManager.getUser(info), size); } else { ResolveInfo info = (ResolveInfo) o; return new WidgetCacheKey( @@ -176,13 +157,7 @@ public class WidgetPreviewLoader { values.put(CacheDb.COLUMN_VERSION, versions[0]); values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview)); - - try { - mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values, - SQLiteDatabase.CONFLICT_REPLACE); - } catch (SQLException e) { - Log.e(TAG, "Error saving image to DB", e); - } + mDb.insertOrReplace(values); } public void removePackage(String packageName, UserHandleCompat user) { @@ -194,13 +169,9 @@ public class WidgetPreviewLoader { mPackageVersions.remove(packageName); } - try { - mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", - new String[] {packageName, Long.toString(userSerial)}); - } catch (SQLException e) { - Log.e(TAG, "Unable to delete items from DB", e); - } + mDb.delete( + CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", + new String[]{packageName, Long.toString(userSerial)}); } /** @@ -222,7 +193,7 @@ public class WidgetPreviewLoader { pkg = ((ResolveInfo) obj).activityInfo.packageName; } else { LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj; - user = mManager.getUser(info); + user = mWidgetManager.getUser(info); pkg = info.provider.getPackageName(); } @@ -238,10 +209,10 @@ public class WidgetPreviewLoader { LongSparseArray> packagesToDelete = new LongSparseArray<>(); Cursor c = null; try { - c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME, - new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, - CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, - null, null, null, null, null); + c = mDb.query( + new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, + CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, + null, null); while (c.moveToNext()) { long userId = c.getLong(0); String pkg = c.getString(1); @@ -274,7 +245,7 @@ public class WidgetPreviewLoader { } } } catch (SQLException e) { - Log.e(TAG, "Error updatating widget previews", e); + Log.e(TAG, "Error updating widget previews", e); } finally { if (c != null) { c.close(); @@ -288,16 +259,15 @@ public class WidgetPreviewLoader { @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { Cursor cursor = null; try { - cursor = mDb.getReadableDatabase().query( - CacheDb.TABLE_NAME, - new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, - CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?", - new String[] { + cursor = mDb.query( + new String[]{CacheDb.COLUMN_PREVIEW_BITMAP}, + CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + + CacheDb.COLUMN_SIZE + " = ?", + new String[]{ key.componentName.flattenToString(), Long.toString(mUserManager.getSerialNumberForUser(key.user)), key.size - }, - null, null, null); + }); // If cancelled, skip getting the blob and decoding it into a bitmap if (loadTask.isCancelled()) { return null; @@ -335,6 +305,17 @@ public class WidgetPreviewLoader { } } + /** + * Generates the widget preview from either the {@link AppWidgetManagerCompat} or cache + * and add badge at the bottom right corner. + * + * @param launcher + * @param info information about the widget + * @param maxPreviewWidth width of the preview on either workspace or tray + * @param preview bitmap that can be recycled + * @param preScaledWidthOut return the width of the returned bitmap + * @return + */ public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info, int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) { // Load the preview image if possible @@ -342,7 +323,7 @@ public class WidgetPreviewLoader { Drawable drawable = null; if (info.previewImage != 0) { - drawable = mManager.loadPreview(info); + drawable = mWidgetManager.loadPreview(info); if (drawable != null) { drawable = mutateOnMainThread(drawable); } else { @@ -357,6 +338,7 @@ public class WidgetPreviewLoader { int previewWidth; int previewHeight; + Bitmap tileBitmap = null; if (widgetPreviewExists) { @@ -428,8 +410,9 @@ public class WidgetPreviewLoader { float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale); try { - Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache)); + Drawable icon = mWidgetManager.loadIcon(info, mIconCache); if (icon != null) { + icon = mutateOnMainThread(icon); int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x; int yoffset = (int) ((tileH - appIconSize * iconScale) / 2); icon.setBounds(hoffset, yoffset, @@ -437,11 +420,13 @@ public class WidgetPreviewLoader { yoffset + (int) (appIconSize * iconScale)); icon.draw(c); } - } catch (Resources.NotFoundException e) { } + } catch (Resources.NotFoundException e) { + } c.setBitmap(null); } + int imageWidth = Math.min(preview.getWidth(), previewWidth + mProfileBadgeMargin); int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin); - return mManager.getBadgeBitmap(info, preview, imageHeight); + return mWidgetManager.getBadgeBitmap(info, preview, imageWidth, imageHeight); } private Bitmap generateShortcutPreview( diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 856e3b88ad..bf33ccf79f 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -67,6 +67,8 @@ import com.android.launcher3.UninstallDropTarget.UninstallSource; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate; +import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; +import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.Thunk; @@ -179,22 +181,20 @@ public class Workspace extends PagedView // in all apps or customize mode) enum State { - NORMAL (SearchDropTargetBar.State.SEARCH_BAR), - NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE), - SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET), - OVERVIEW (SearchDropTargetBar.State.INVISIBLE), - OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE); + NORMAL (SearchDropTargetBar.State.SEARCH_BAR, false), + NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE_TRANSLATED, false), + SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET, false), + OVERVIEW (SearchDropTargetBar.State.INVISIBLE, true), + OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE, true); - private final SearchDropTargetBar.State mBarState; + public final SearchDropTargetBar.State searchDropTargetBarState; + public final boolean shouldUpdateWidget; - State(SearchDropTargetBar.State searchBarState) { - mBarState = searchBarState; + State(SearchDropTargetBar.State searchBarState, boolean shouldUpdateWidget) { + searchDropTargetBarState = searchBarState; + this.shouldUpdateWidget = shouldUpdateWidget; } - - public SearchDropTargetBar.State getSearchDropTargetBarState() { - return mBarState; - } - }; + } private State mState = State.NORMAL; private boolean mIsSwitchingState = false; @@ -358,7 +358,7 @@ public class Workspace extends PagedView if (getChildCount() > 0) { // Use the first non-custom page to estimate the child position CellLayout cl = (CellLayout) getChildAt(numCustomPages()); - Rect r = estimateItemPosition(cl, itemInfo, 0, 0, itemInfo.spanX, itemInfo.spanY); + Rect r = estimateItemPosition(cl, 0, 0, itemInfo.spanX, itemInfo.spanY); size[0] = r.width(); size[1] = r.height(); if (springLoaded) { @@ -373,8 +373,7 @@ public class Workspace extends PagedView } } - public Rect estimateItemPosition(CellLayout cl, ItemInfo pendingInfo, - int hCell, int vCell, int hSpan, int vSpan) { + public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) { Rect r = new Rect(); cl.cellToRect(hCell, vCell, hSpan, vSpan, r); return r; @@ -578,6 +577,7 @@ public class Workspace extends PagedView CellLayout customScreen = (CellLayout) mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, this, false); customScreen.disableDragTarget(); + customScreen.disableJailContent(); mWorkspaceScreens.put(CUSTOM_CONTENT_SCREEN_ID, customScreen); mScreenOrder.add(0, CUSTOM_CONTENT_SCREEN_ID); @@ -649,6 +649,10 @@ public class Workspace extends PagedView parent.removeView(customContent); } customScreen.removeAllViews(); + customContent.setFocusable(true); + customContent.setOnKeyListener(new FullscreenKeyEventListener()); + customContent.setOnFocusChangeListener(mLauncher.mFocusHandler + .getHideIndicatorOnFocusListener()); customScreen.addViewToCellLayout(customContent, 0, 0, lp, true); mCustomContentDescription = description; @@ -1118,9 +1122,9 @@ public class Workspace extends PagedView LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) info.hostView; if (lahv != null && lahv.isReinflateRequired()) { - mLauncher.removeAppWidget(info); - // Remove the current widget which is inflated with the wrong orientation - cl.removeView(lahv); + // Remove and rebind the current widget (which was inflated in the wrong + // orientation), but don't delete it from the database + mLauncher.removeItem(lahv, info, false /* deleteFromDb */); mLauncher.bindAppWidget(info); } } @@ -1326,7 +1330,7 @@ public class Workspace extends PagedView mLauncher.overrideWallpaperDimensions()); return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); } protected void snapToPage(int whichPage, Runnable r) { @@ -1410,7 +1414,7 @@ public class Workspace extends PagedView return false; } - private float wallpaperOffsetForCurrentScroll() { + public float wallpaperOffsetForScroll(int scroll) { // TODO: do different behavior if it's a live wallpaper? // Don't use up all the wallpaper parallax until you have at least // MIN_PARALLAX_PAGE_SPAN pages @@ -1449,7 +1453,7 @@ public class Workspace extends PagedView // Sometimes the left parameter of the pages is animated during a layout transition; // this parameter offsets it to keep the wallpaper from animating as well int adjustedScroll = - getScrollX() - firstPageScrollX - getLayoutTransitionOffsetForPage(0); + scroll - firstPageScrollX - getLayoutTransitionOffsetForPage(0); float offset = Math.min(1, adjustedScroll / (float) scrollRange); offset = Math.max(0, offset); @@ -1463,6 +1467,10 @@ public class Workspace extends PagedView } } + private float wallpaperOffsetForCurrentScroll() { + return wallpaperOffsetForScroll(getScrollX()); + } + private int numEmptyScreensToIgnore() { int numScrollingPages = getChildCount() - numCustomPages(); if (numScrollingPages >= MIN_PARALLAX_PAGE_SPAN && hasExtraEmptyScreen()) { @@ -1536,6 +1544,13 @@ public class Workspace extends PagedView mWallpaperOffset.syncWithScroll(); } + @Override + protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { + if (!isSwitchingState()) { + super.determineScrollingStart(ev, touchSlopScale); + } + } + @Override public void announceForAccessibility(CharSequence text) { // Don't announce if apps is on top of us. @@ -1606,7 +1621,7 @@ public class Workspace extends PagedView } public boolean isOnOrMovingToCustomContent() { - return hasCustomContent() && getNextPage() == 0; + return hasCustomContent() && getNextPage() == 0 && mRestorePage == INVALID_RESTORE_PAGE; } private void updateStateForCustomContent(int screenCenter) { @@ -2001,10 +2016,16 @@ public class Workspace extends PagedView Animator workspaceAnim = mStateTransitionAnimation.getAnimationToState(mState, toState, toPage, animated, layerViews); + boolean shouldNotifyWidgetChange = !mState.shouldUpdateWidget + && toState.shouldUpdateWidget; // Update the current state mState = toState; updateAccessibilityFlags(); + if (shouldNotifyWidgetChange) { + mLauncher.notifyWidgetProvidersChanged(); + } + return workspaceAnim; } @@ -2020,7 +2041,7 @@ public class Workspace extends PagedView } setImportantForAccessibility((mState == State.NORMAL || mState == State.OVERVIEW) ? IMPORTANT_FOR_ACCESSIBILITY_AUTO - : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } else { int accessible = mState == State.NORMAL ? IMPORTANT_FOR_ACCESSIBILITY_AUTO : @@ -2054,6 +2075,7 @@ public class Workspace extends PagedView @Override public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { mIsSwitchingState = true; + mTransitionProgress = 0; // Invalidate here to ensure that the pages are rendered during the state change transition. invalidate(); @@ -2469,14 +2491,17 @@ public class Workspace extends PagedView return true; } - boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float - distance, boolean considerTimeout) { + boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, + float distance, boolean considerTimeout) { if (distance > mMaxDistanceForFolderCreation) return false; View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + return willCreateUserFolder(info, dropOverView, considerTimeout); + } + boolean willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout) { if (dropOverView != null) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); - if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) { return false; } } @@ -2493,7 +2518,7 @@ public class Workspace extends PagedView boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo); boolean willBecomeShortcut = (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || - info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT); + info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT); return (aboveShortcut && willBecomeShortcut); } @@ -2502,10 +2527,13 @@ public class Workspace extends PagedView float distance) { if (distance > mMaxDistanceForFolderCreation) return false; View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + return willAddToExistingUserFolder(dragInfo, dropOverView); + } + boolean willAddToExistingUserFolder(Object dragInfo, View dropOverView) { if (dropOverView != null) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); - if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) { return false; } } @@ -2534,7 +2562,7 @@ public class Workspace extends PagedView if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false; mCreateUserFolderOnDrop = false; - final long screenId = (targetCell == null) ? mDragInfo.screenId : getIdForScreen(target); + final long screenId = getIdForScreen(target); boolean aboveShortcut = (v.getTag() instanceof ShortcutInfo); boolean willBecomeShortcut = (newView.getTag() instanceof ShortcutInfo); @@ -3205,8 +3233,6 @@ public class Workspace extends PagedView mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null); } - ItemInfo info = (ItemInfo) d.dragInfo; - int minSpanX = item.spanX; int minSpanY = item.spanY; if (item.minSpanX > 0 && item.minSpanY > 0) { @@ -3225,11 +3251,7 @@ public class Workspace extends PagedView float targetCellDistance = mDragTargetLayout.getDistanceFromCell( mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell); - final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], - mTargetCell[1]); - - manageFolderFeedback(info, mDragTargetLayout, mTargetCell, - targetCellDistance, dragOverView, d.accessibleDrag); + manageFolderFeedback(mDragTargetLayout, mTargetCell, targetCellDistance, d); boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, @@ -3237,9 +3259,7 @@ public class Workspace extends PagedView if (!nearestDropOccupied) { mDragTargetLayout.visualizeDropLocation(child, mDragOutline, - (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], - mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, - d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion()); + mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d); } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER) && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX || mLastReorderY != reorderY)) { @@ -3252,7 +3272,7 @@ public class Workspace extends PagedView // Otherwise, if we aren't adding to or creating a folder and there's no pending // reorder, then we schedule a reorder ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter, - minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child); + minSpanX, minSpanY, item.spanX, item.spanY, d, child); mReorderAlarm.setOnAlarmListener(listener); mReorderAlarm.setAlarm(REORDER_TIMEOUT); } @@ -3266,28 +3286,34 @@ public class Workspace extends PagedView } } - private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, - int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) { - boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, - false); + private void manageFolderFeedback(CellLayout targetLayout, + int[] targetCell, float distance, DragObject dragObject) { + if (distance > mMaxDistanceForFolderCreation) return; + + final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]); + ItemInfo info = (ItemInfo) dragObject.dragInfo; + boolean userFolderPending = willCreateUserFolder(info, dragOverView, false); if (mDragMode == DRAG_MODE_NONE && userFolderPending && !mFolderCreationAlarm.alarmPending()) { FolderCreationAlarmListener listener = new FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]); - if (!accessibleDrag) { + if (!dragObject.accessibleDrag) { mFolderCreationAlarm.setOnAlarmListener(listener); mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); } else { listener.onAlarm(mFolderCreationAlarm); } + + if (dragObject.stateAnnouncer != null) { + dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper + .getDescriptionForDropOver(dragOverView, getContext())); + } return; } - boolean willAddToFolder = - willAddToExistingUserFolder(info, targetLayout, targetCell, distance); - + boolean willAddToFolder = willAddToExistingUserFolder(info, dragOverView); if (willAddToFolder && mDragMode == DRAG_MODE_NONE) { mDragOverFolderIcon = ((FolderIcon) dragOverView); mDragOverFolderIcon.onDragEnter(info); @@ -3295,6 +3321,11 @@ public class Workspace extends PagedView targetLayout.clearDragOutlines(); } setDragMode(DRAG_MODE_ADD_TO_FOLDER); + + if (dragObject.stateAnnouncer != null) { + dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper + .getDescriptionForDropOver(dragOverView, getContext())); + } return; } @@ -3304,8 +3335,6 @@ public class Workspace extends PagedView if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) { setDragMode(DRAG_MODE_NONE); } - - return; } class FolderCreationAlarmListener implements OnAlarmListener { @@ -3337,18 +3366,18 @@ public class Workspace extends PagedView class ReorderAlarmListener implements OnAlarmListener { float[] dragViewCenter; int minSpanX, minSpanY, spanX, spanY; - DragView dragView; + DragObject dragObject; View child; public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX, - int spanY, DragView dragView, View child) { + int spanY, DragObject dragObject, View child) { this.dragViewCenter = dragViewCenter; this.minSpanX = minSpanX; this.minSpanY = minSpanY; this.spanX = spanX; this.spanY = spanY; this.child = child; - this.dragView = dragView; + this.dragObject = dragObject; } public void onAlarm(Alarm alarm) { @@ -3371,9 +3400,7 @@ public class Workspace extends PagedView boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY; mDragTargetLayout.visualizeDropLocation(child, mDragOutline, - (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], - mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, - dragView.getDragVisualizeOffset(), dragView.getDragRegion()); + mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject); } } @@ -3492,11 +3519,11 @@ public class Workspace extends PagedView boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - View finalView = isWidget ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + AppWidgetHostView finalView = isWidget ? + ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; - if (finalView instanceof AppWidgetHostView && updateWidgetSize) { - AppWidgetHostView awhv = (AppWidgetHostView) finalView; - AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, item.spanX, + if (finalView != null && updateWidgetSize) { + AppWidgetResizeFrame.updateWidgetSizeRanges(finalView, mLauncher, item.spanX, item.spanY); } @@ -3596,14 +3623,13 @@ public class Workspace extends PagedView } private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY, - DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, - boolean external, boolean scale) { + DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale) { // Now we animate the dragView, (ie. the widget or shortcut preview) into its final // location and size on the home screen. int spanX = info.spanX; int spanY = info.spanY; - Rect r = estimateItemPosition(layout, info, targetCell[0], targetCell[1], spanX, spanY); + Rect r = estimateItemPosition(layout, targetCell[0], targetCell[1], spanX, spanY); loc[0] = r.left; loc[1] = r.top; @@ -3624,14 +3650,15 @@ public class Workspace extends PagedView // The animation will scale the dragView about its center, so we need to center about // the final location. - loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2; + loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2 + - Math.ceil(layout.getUnusedHorizontalSpace() / 2f); loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2; scaleXY[0] = dragViewScaleX * cellLayoutScale; scaleXY[1] = dragViewScaleY * cellLayoutScale; } - public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, DragView dragView, + public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView, final Runnable onCompleteRunnable, int animationType, final View finalView, boolean external) { Rect from = new Rect(); @@ -3641,16 +3668,11 @@ public class Workspace extends PagedView float scaleXY[] = new float[2]; boolean scalePreview = !(info instanceof PendingAddShortcutInfo); getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell, - external, scalePreview); + scalePreview); Resources res = mLauncher.getResources(); final int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200; - // In the case where we've prebound the widget, we remove it from the DragLayer - if (finalView instanceof AppWidgetHostView && external) { - mLauncher.getDragLayer().removeView(finalView); - } - boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) { @@ -3670,7 +3692,7 @@ public class Workspace extends PagedView if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) { endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE; } else { - endStyle = DragLayer.ANIMATION_END_DISAPPEAR;; + endStyle = DragLayer.ANIMATION_END_DISAPPEAR; } Runnable onComplete = new Runnable() { @@ -4143,41 +4165,10 @@ public class Workspace extends PagedView }); } - public void disableShortcutsByPackageName(final ArrayList packages, - final UserHandleCompat user, final int reason) { - final HashSet packageNames = new HashSet(); - packageNames.addAll(packages); - - mapOverItems(MAP_RECURSE, new ItemOperator() { - @Override - public boolean evaluate(ItemInfo info, View v, View parent) { - if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - ComponentName cn = shortcutInfo.getTargetComponent(); - if (user.equals(shortcutInfo.user) && cn != null - && packageNames.contains(cn.getPackageName())) { - shortcutInfo.isDisabled |= reason; - BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache); - - if (parent != null) { - parent.invalidate(); - } - } - } - // process all the shortcuts - return false; - } - }); - } - // Removes ALL items that match a given package name, this is usually called when a package // has been removed and we want to remove all components (widgets, shortcuts, apps) that // belong to that package. - void removeItemsByPackageName(final ArrayList packages, final UserHandleCompat user) { - final HashSet packageNames = new HashSet(); - packageNames.addAll(packages); - + void removeItemsByPackageName(final HashSet packageNames, final UserHandleCompat user) { // Filter out all the ItemInfos that this is going to affect final HashSet infos = new HashSet(); final HashSet cns = new HashSet(); @@ -4359,7 +4350,7 @@ public class Workspace extends PagedView } public void removeAbandonedPromise(String packageName, UserHandleCompat user) { - ArrayList packages = new ArrayList(1); + HashSet packages = new HashSet<>(1); packages.add(packageName); LauncherModel.deletePackageFromDatabase(mLauncher, packageName, user); removeItemsByPackageName(packages, user); @@ -4383,13 +4374,22 @@ public class Workspace extends PagedView }); } - void widgetsRestored(ArrayList changedInfo) { + public void widgetsRestored(ArrayList changedInfo) { if (!changedInfo.isEmpty()) { DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo, mLauncher.getAppWidgetHost()); - if (LauncherModel.getProviderInfo(getContext(), - changedInfo.get(0).providerName, - changedInfo.get(0).user) != null) { + + LauncherAppWidgetInfo item = changedInfo.get(0); + final AppWidgetProviderInfo widgetInfo; + if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { + widgetInfo = AppWidgetManagerCompat + .getInstance(mLauncher).findProvider(item.providerName, item.user); + } else { + widgetInfo = AppWidgetManagerCompat.getInstance(mLauncher) + .getAppWidgetInfo(item.appWidgetId); + } + + if (widgetInfo != null) { // Re-inflate the widgets which have changed status widgetRefresh.run(); } else { @@ -4468,8 +4468,21 @@ public class Workspace extends PagedView private String getPageDescription(int page) { int delta = numCustomPages(); - return String.format(getContext().getString(R.string.workspace_scroll_format), - page + 1 - delta, getChildCount() - delta); + int nScreens = getChildCount() - delta; + int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID); + if (extraScreenId >= 0 && nScreens > 1) { + if (page == extraScreenId) { + return getContext().getString(R.string.workspace_new_page); + } + nScreens--; + } + if (nScreens == 0) { + // When the workspace is not loaded, we do not know how many screen will be bound. + return getContext().getString(R.string.all_apps_home_button_label); + } + return getContext().getString(R.string.workspace_scroll_format, + page + 1 - delta, nScreens); + } public void getLocationInDragLayer(int[] loc) { @@ -4477,7 +4490,7 @@ public class Workspace extends PagedView } @Override - public void fillInLaunchSourceData(Bundle sourceData) { + public void fillInLaunchSourceData(View v, Bundle sourceData) { sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOMESCREEN); sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage()); } @@ -4519,12 +4532,9 @@ public class Workspace extends PagedView for (LauncherAppWidgetInfo info : mInfos) { if (info.hostView instanceof PendingAppWidgetHostView) { + // Remove and rebind the current widget, but don't delete it from the database PendingAppWidgetHostView view = (PendingAppWidgetHostView) info.hostView; - mLauncher.removeAppWidget(info); - - CellLayout cl = (CellLayout) view.getParent().getParent(); - // Remove the current widget - cl.removeView(view); + mLauncher.removeItem(view, info, false /* deleteFromDb */); mLauncher.bindAppWidget(info); } } diff --git a/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java b/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java new file mode 100644 index 0000000000..8ff82dd4f8 --- /dev/null +++ b/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.accessibility; + +import android.content.Context; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +/** + * Periodically sends accessibility events to announce ongoing state changed. Based on the + * implementation in ProgressBar. + */ +public class DragViewStateAnnouncer implements Runnable { + + private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; + + private final View mTargetView; + + private DragViewStateAnnouncer(View view) { + mTargetView = view; + } + + public void announce(CharSequence msg) { + mTargetView.setContentDescription(msg); + mTargetView.removeCallbacks(this); + mTargetView.postDelayed(this, TIMEOUT_SEND_ACCESSIBILITY_EVENT); + } + + public void cancel() { + mTargetView.removeCallbacks(this); + } + + @Override + public void run() { + mTargetView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + + public static DragViewStateAnnouncer createFor(View v) { + if (((AccessibilityManager) v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)) + .isEnabled()) { + return new DragViewStateAnnouncer(v); + } else { + return null; + } + } +} diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index fe7b25edd3..2306b776ed 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -134,11 +134,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme public boolean performAction(final View host, final ItemInfo item, int action) { if (action == REMOVE) { - if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) { - announceConfirmation(R.string.item_removed); - return true; - } - return false; + DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host); + return true; } else if (action == INFO) { InfoDropTarget.startDetailsActivityForInfo(item, mLauncher); return true; @@ -175,7 +172,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return true; } else if (action == MOVE_TO_WORKSPACE) { Folder folder = mLauncher.getWorkspace().getOpenFolder(); - mLauncher.closeFolder(folder); + mLauncher.closeFolder(folder, true); ShortcutInfo info = (ShortcutInfo) item; folder.getInfo().remove(info); diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index 80ddc13b7c..73b824bb57 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -16,6 +16,7 @@ package com.android.launcher3.accessibility; +import android.content.Context; import android.text.TextUtils; import android.view.View; @@ -140,26 +141,30 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega return mContext.getString(R.string.move_to_empty_cell, y + 1, x + 1); } } else { - ItemInfo info = (ItemInfo) child.getTag(); - if (info instanceof ShortcutInfo) { - return mContext.getString(R.string.create_folder_with, info.title); - } else if (info instanceof FolderInfo) { - if (TextUtils.isEmpty(info.title)) { - // Find the first item in the folder. - FolderInfo folder = (FolderInfo) info; - ShortcutInfo firstItem = null; - for (ShortcutInfo shortcut : folder.contents) { - if (firstItem == null || firstItem.rank > shortcut.rank) { - firstItem = shortcut; - } - } + return getDescriptionForDropOver(child, mContext); + } + } - if (firstItem != null) { - return mContext.getString(R.string.add_to_folder_with_app, firstItem.title); + public static String getDescriptionForDropOver(View overChild, Context context) { + ItemInfo info = (ItemInfo) overChild.getTag(); + if (info instanceof ShortcutInfo) { + return context.getString(R.string.create_folder_with, info.title); + } else if (info instanceof FolderInfo) { + if (TextUtils.isEmpty(info.title)) { + // Find the first item in the folder. + FolderInfo folder = (FolderInfo) info; + ShortcutInfo firstItem = null; + for (ShortcutInfo shortcut : folder.contents) { + if (firstItem == null || firstItem.rank > shortcut.rank) { + firstItem = shortcut; } } - return mContext.getString(R.string.add_to_folder, info.title); + + if (firstItem != null) { + return context.getString(R.string.add_to_folder_with_app, firstItem.title); + } } + return context.getString(R.string.add_to_folder, info.title); } return ""; } diff --git a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java index 117aca921b..dafa73fecd 100644 --- a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java +++ b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java @@ -35,6 +35,7 @@ class TransformedImageDrawable { private float mXPercent; private float mYPercent; private int mGravity; + private int mAlpha; /** * @param gravity If one of the Gravity center values, the x and y offset will take the width @@ -50,10 +51,11 @@ class TransformedImageDrawable { public void setAlpha(int alpha) { mImage.setAlpha(alpha); + mAlpha = alpha; } public int getAlpha() { - return mImage.getAlpha(); + return mAlpha; } public void updateBounds(Rect bounds) { diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 88c6acadac..c12f645e31 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -17,29 +17,29 @@ package com.android.launcher3.allapps; import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.drawable.InsetDrawable; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.LinearLayout; + import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; +import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Folder; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; @@ -48,7 +48,6 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.Thunk; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; @@ -133,19 +132,22 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; private static final int MAX_NUM_MERGES_PHONE = 2; - @Thunk Launcher mLauncher; - @Thunk AlphabeticalAppsList mApps; - private AllAppsGridAdapter mAdapter; - private RecyclerView.LayoutManager mLayoutManager; - private RecyclerView.ItemDecoration mItemDecoration; + private final Launcher mLauncher; + private final AlphabeticalAppsList mApps; + private final AllAppsGridAdapter mAdapter; + private final RecyclerView.LayoutManager mLayoutManager; + private final RecyclerView.ItemDecoration mItemDecoration; + + // The computed bounds of the container + private final Rect mContentBounds = new Rect(); + + private AllAppsRecyclerView mAppsRecyclerView; + private AllAppsSearchBarController mSearchBarController; + + private View mSearchContainer; + private ExtendedEditText mSearchInput; + private HeaderElevationController mElevationController; - @Thunk View mContent; - @Thunk View mContainerView; - @Thunk View mRevealView; - @Thunk AllAppsRecyclerView mAppsRecyclerView; - @Thunk AllAppsSearchBarController mSearchBarController; - private ViewGroup mSearchBarContainerView; - private View mSearchBarView; private SpannableStringBuilder mSearchQueryBuilder = null; private int mSectionNamesMargin; @@ -157,14 +159,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // This coordinate is relative to its parent private final Point mIconLastTouchPos = new Point(); - private View.OnClickListener mSearchClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent searchIntent = (Intent) v.getTag(); - mLauncher.startActivitySafely(v, searchIntent, null); - } - }; - public AllAppsContainerView(Context context) { this(context, null); } @@ -234,14 +228,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc throw new RuntimeException("Expected search bar controller to only be set once"); } mSearchBarController = searchController; - mSearchBarController.initialize(mApps, this); - - // Add the new search view to the layout - View searchBarView = searchController.getView(mSearchBarContainerView); - mSearchBarContainerView.addView(searchBarView); - mSearchBarContainerView.setVisibility(View.VISIBLE); - mSearchBarView = searchBarView; - setHasSearchBar(); + mSearchBarController.initialize(mApps, mSearchInput, mLauncher, this); + mAdapter.setSearchController(mSearchBarController); updateBackgroundAndPaddings(); } @@ -253,34 +241,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mAppsRecyclerView.scrollToTop(); } - /** - * Returns the content view used for the launcher transitions. - */ - public View getContentView() { - return mContainerView; - } - - /** - * Returns the all apps search view. - */ - public View getSearchBarView() { - return mSearchBarView; - } - - /** - * Returns the reveal view used for the launcher transitions. - */ - public View getRevealView() { - return mRevealView; - } - - /** - * Returns an new instance of the default app search controller. - */ - public AllAppsSearchBarController newDefaultAppSearchController() { - return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView); - } - /** * Focuses the search field and begins an app search. */ @@ -290,28 +250,35 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc } } + /** + * Resets the state of AllApps. + */ + public void reset() { + // Reset the search bar and base recycler view after transitioning home + mSearchBarController.reset(); + mAppsRecyclerView.reset(); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); - boolean isRtl = Utilities.isRtl(getResources()); - mAdapter.setRtl(isRtl); - mContent = findViewById(R.id.content); // This is a focus listener that proxies focus from a view into the list view. This is to // work around the search box from getting first focus and showing the cursor. - View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() { + getContentView().setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { mAppsRecyclerView.requestFocus(); } } - }; - mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container); - mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener); - mContainerView = findViewById(R.id.all_apps_container); - mContainerView.setOnFocusChangeListener(focusProxyListener); - mRevealView = findViewById(R.id.all_apps_reveal); + }); + + mSearchContainer = findViewById(R.id.search_container); + mSearchInput = (ExtendedEditText) findViewById(R.id.search_box_input); + mElevationController = Utilities.ATLEAST_LOLLIPOP + ? new HeaderElevationController.ControllerVL(mSearchContainer) + : new HeaderElevationController.ControllerV16(mSearchContainer); // Load the all apps recycler view mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); @@ -319,10 +286,31 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mAppsRecyclerView.setLayoutManager(mLayoutManager); mAppsRecyclerView.setAdapter(mAdapter); mAppsRecyclerView.setHasFixedSize(true); + mAppsRecyclerView.addOnScrollListener(mElevationController); + mAppsRecyclerView.setElevationController(mElevationController); + if (mItemDecoration != null) { mAppsRecyclerView.addItemDecoration(mItemDecoration); } + // Precalculate the prediction icon and normal icon sizes + LayoutInflater layoutInflater = LayoutInflater.from(getContext()); + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec( + getResources().getDisplayMetrics().widthPixels, MeasureSpec.AT_MOST); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec( + getResources().getDisplayMetrics().heightPixels, MeasureSpec.AT_MOST); + + BubbleTextView icon = (BubbleTextView) layoutInflater.inflate( + R.layout.all_apps_icon, this, false); + icon.applyDummyInfo(); + icon.measure(widthMeasureSpec, heightMeasureSpec); + BubbleTextView predIcon = (BubbleTextView) layoutInflater.inflate( + R.layout.all_apps_prediction_bar_icon, this, false); + predIcon.applyDummyInfo(); + predIcon.measure(widthMeasureSpec, heightMeasureSpec); + mAppsRecyclerView.setPremeasuredIconHeights(predIcon.getMeasuredHeight(), + icon.getMeasuredHeight()); + updateBackgroundAndPaddings(); } @@ -333,9 +321,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mContentBounds.set(mContentPadding.left, mContentPadding.top, + MeasureSpec.getSize(widthMeasureSpec) - mContentPadding.right, + MeasureSpec.getSize(heightMeasureSpec) - mContentPadding.bottom); + // Update the number of items in the grid before we measure the view - int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : - MeasureSpec.getSize(widthMeasureSpec); + // TODO: mSectionNamesMargin is currently 0, but also account for it, + // if it's enabled in the future. + int availableWidth = (!mContentBounds.isEmpty() ? mContentBounds.width() : + MeasureSpec.getSize(widthMeasureSpec)) + - 2 * mAppsRecyclerView.getMaxScrollbarWidth(); DeviceProfile grid = mLauncher.getDeviceProfile(); grid.updateAppsViewNumCols(getResources(), availableWidth); if (mNumAppsPerRow != grid.allAppsNumCols || @@ -354,6 +349,12 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); mAdapter.setNumAppsPerRow(mNumAppsPerRow); mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm); + + if (mNumAppsPerRow > 0) { + int iconSize = availableWidth / mNumAppsPerRow; + int iconSpacing = (iconSize - grid.allAppsIconSizePx) / 2; + mSearchInput.setPaddingRelative(iconSpacing, 0, iconSpacing, 0); + } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -365,51 +366,28 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc * recycler view to handle touch events (for fast scrolling) all the way to the edge. */ @Override - protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { - boolean isRtl = Utilities.isRtl(getResources()); - - // TODO: Use quantum_panel instead of quantum_panel_shape - InsetDrawable background = new InsetDrawable( - getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, - padding.right, 0); - Rect bgPadding = new Rect(); - background.getPadding(bgPadding); - mContainerView.setBackground(background); - mRevealView.setBackground(background.getConstantState().newDrawable()); + protected void onUpdateBgPadding(Rect padding, Rect bgPadding) { mAppsRecyclerView.updateBackgroundPadding(bgPadding); mAdapter.updateBackgroundPadding(bgPadding); - - // Hack: We are going to let the recycler view take the full width, so reset the padding on - // the container to zero after setting the background and apply the top-bottom padding to - // the content view instead so that the launcher transition clips correctly. - mContent.setPadding(0, padding.top, 0, padding.bottom); - mContainerView.setPadding(0, 0, 0, 0); + mElevationController.updateBackgroundPadding(bgPadding); // Pad the recycler view by the background padding plus the start margin (for the section // names) - int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth()); + int maxScrollBarWidth = mAppsRecyclerView.getMaxScrollbarWidth(); + int startInset = Math.max(mSectionNamesMargin, maxScrollBarWidth); int topBottomPadding = mRecyclerViewTopBottomPadding; - if (isRtl) { - mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), + if (Utilities.isRtl(getResources())) { + mAppsRecyclerView.setPadding(padding.left + maxScrollBarWidth, topBottomPadding, padding.right + startInset, topBottomPadding); } else { mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding, - padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding); + padding.right + maxScrollBarWidth, topBottomPadding); } - // Inset the search bar to fit its bounds above the container - if (mSearchBarView != null) { - Rect backgroundPadding = new Rect(); - if (mSearchBarView.getBackground() != null) { - mSearchBarView.getBackground().getPadding(backgroundPadding); - } - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) - mSearchBarContainerView.getLayoutParams(); - lp.leftMargin = searchBarBounds.left - backgroundPadding.left; - lp.topMargin = searchBarBounds.top - backgroundPadding.top; - lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right; - mSearchBarContainerView.requestLayout(); - } + MarginLayoutParams lp = (MarginLayoutParams) mSearchContainer.getLayoutParams(); + lp.leftMargin = padding.left; + lp.rightMargin = padding.right; + mSearchContainer.setLayoutParams(lp); } @Override @@ -555,9 +533,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { if (toWorkspace) { - // Reset the search bar and base recycler view after transitioning home - mSearchBarController.reset(); - mAppsRecyclerView.reset(); + reset(); } } @@ -613,16 +589,18 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onSearchResult(String query, ArrayList apps) { if (apps != null) { - mApps.setOrderedFilter(apps); + if (mApps.setOrderedFilter(apps)) { + mAppsRecyclerView.onSearchResultsChanged(); + } mAdapter.setLastSearchQuery(query); - mAppsRecyclerView.onSearchResultsChanged(); } } @Override public void clearSearchResult() { - mApps.setOrderedFilter(null); - mAppsRecyclerView.onSearchResultsChanged(); + if (mApps.setOrderedFilter(null)) { + mAppsRecyclerView.onSearchResultsChanged(); + } // Clear the search query mSearchQueryBuilder.clear(); diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java new file mode 100644 index 0000000000..73de45e397 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.BaseRecyclerViewFastScrollBar; +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.util.Thunk; + +import java.util.HashSet; + +public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback { + + private static final int INITIAL_TOUCH_SETTLING_DURATION = 100; + private static final int REPEAT_TOUCH_SETTLING_DURATION = 200; + private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f; + + private AllAppsRecyclerView mRv; + private AlphabeticalAppsList mApps; + + // Keeps track of the current and targetted fast scroll section (the section to scroll to after + // the initial delay) + int mTargetFastScrollPosition = -1; + @Thunk String mCurrentFastScrollSection; + @Thunk String mTargetFastScrollSection; + + // The settled states affect the delay before the fast scroll animation is applied + private boolean mHasFastScrollTouchSettled; + private boolean mHasFastScrollTouchSettledAtLeastOnce; + + // Set of all views animated during fast scroll. We keep track of these ourselves since there + // is no way to reset a view once it gets scrapped or recycled without other hacks + private HashSet mTrackedFastScrollViews = + new HashSet<>(); + + // Smooth fast-scroll animation frames + @Thunk int mFastScrollFrameIndex; + @Thunk final int[] mFastScrollFrames = new int[10]; + + /** + * This runnable runs a single frame of the smooth scroll animation and posts the next frame + * if necessary. + */ + @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { + @Override + public void run() { + if (mFastScrollFrameIndex < mFastScrollFrames.length) { + mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); + mFastScrollFrameIndex++; + mRv.postOnAnimation(mSmoothSnapNextFrameRunnable); + } + } + }; + + /** + * This runnable updates the current fast scroll section to the target fastscroll section. + */ + Runnable mFastScrollToTargetSectionRunnable = new Runnable() { + @Override + public void run() { + // Update to the target section + mCurrentFastScrollSection = mTargetFastScrollSection; + mHasFastScrollTouchSettled = true; + mHasFastScrollTouchSettledAtLeastOnce = true; + updateTrackedViewsFastScrollFocusState(); + } + }; + + public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) { + mRv = rv; + mApps = apps; + } + + public void onSetAdapter(AllAppsGridAdapter adapter) { + adapter.setBindViewCallback(this); + } + + /** + * Smooth scrolls the recycler view to the given section. + * + * @return whether the fastscroller can scroll to the new section. + */ + public boolean smoothScrollToSection(int scrollY, int availableScrollHeight, + AlphabeticalAppsList.FastScrollSectionInfo info) { + if (mTargetFastScrollPosition != info.fastScrollToItem.position) { + mTargetFastScrollPosition = info.fastScrollToItem.position; + smoothSnapToPosition(scrollY, availableScrollHeight, info); + return true; + } + return false; + } + + /** + * Smoothly snaps to a given position. We do this manually by calculating the keyframes + * ourselves and animating the scroll on the recycler view. + */ + private void smoothSnapToPosition(int scrollY, int availableScrollHeight, + AlphabeticalAppsList.FastScrollSectionInfo info) { + mRv.removeCallbacks(mSmoothSnapNextFrameRunnable); + mRv.removeCallbacks(mFastScrollToTargetSectionRunnable); + + trackAllChildViews(); + if (mHasFastScrollTouchSettled) { + // In this case, the user has already settled once (and the fast scroll state has + // animated) and they are just fine-tuning their section from the last section, so + // we should make it feel fast and update immediately. + mCurrentFastScrollSection = info.sectionName; + mTargetFastScrollSection = null; + updateTrackedViewsFastScrollFocusState(); + } else { + // Otherwise, the user has scrubbed really far, and we don't want to distract the user + // with the flashing fast scroll state change animation in addition to the fast scroll + // section popup, so reset the views to normal, and wait for the touch to settle again + // before animating the fast scroll state. + mCurrentFastScrollSection = null; + mTargetFastScrollSection = info.sectionName; + mHasFastScrollTouchSettled = false; + updateTrackedViewsFastScrollFocusState(); + + // Delay scrolling to a new section until after some duration. If the user has been + // scrubbing a while and makes multiple big jumps, then reduce the time needed for the + // fast scroll to settle so it doesn't feel so long. + mRv.postDelayed(mFastScrollToTargetSectionRunnable, + mHasFastScrollTouchSettledAtLeastOnce ? + REPEAT_TOUCH_SETTLING_DURATION : + INITIAL_TOUCH_SETTLING_DURATION); + } + + // Calculate the full animation from the current scroll position to the final scroll + // position, and then run the animation for the duration. + int newScrollY = Math.min(availableScrollHeight, + mRv.getPaddingTop() + mRv.getTop(info.fastScrollToItem.rowIndex)); + int numFrames = mFastScrollFrames.length; + for (int i = 0; i < numFrames; i++) { + // TODO(winsonc): We can interpolate this as well. + mFastScrollFrames[i] = (newScrollY - scrollY) / numFrames; + } + mFastScrollFrameIndex = 0; + mRv.postOnAnimation(mSmoothSnapNextFrameRunnable); + } + + public void onFastScrollCompleted() { + // TODO(winsonc): Handle the case when the user scrolls and releases before the animation + // runs + + // Stop animating the fast scroll position and state + mRv.removeCallbacks(mSmoothSnapNextFrameRunnable); + mRv.removeCallbacks(mFastScrollToTargetSectionRunnable); + + // Reset the tracking variables + mHasFastScrollTouchSettled = false; + mHasFastScrollTouchSettledAtLeastOnce = false; + mCurrentFastScrollSection = null; + mTargetFastScrollSection = null; + mTargetFastScrollPosition = -1; + + updateTrackedViewsFastScrollFocusState(); + mTrackedFastScrollViews.clear(); + } + + @Override + public void onBindView(AllAppsGridAdapter.ViewHolder holder) { + // Update newly bound views to the current fast scroll state if we are fast scrolling + if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) { + if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + BaseRecyclerViewFastScrollBar.FastScrollFocusableView v = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent; + updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */); + mTrackedFastScrollViews.add(v); + } + } + } + + /** + * Starts tracking all the recycler view's children which are FastScrollFocusableViews. + */ + private void trackAllChildViews() { + int childCount = mRv.getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = mRv.getChildAt(i); + if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v); + } + } + } + + /** + * Updates the fast scroll focus on all the children. + */ + private void updateTrackedViewsFastScrollFocusState() { + for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) { + RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v); + int pos = (viewHolder != null) ? viewHolder.getPosition() : -1; + updateViewFastScrollFocusState(v, pos, true); + } + } + + /** + * Updates the fast scroll focus on all a given view. + */ + private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, + int pos, boolean animated) { + FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL; + if (mCurrentFastScrollSection != null && pos > -1) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos); + boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) && + item.position == mTargetFastScrollPosition; + newState = highlight ? + FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED : + FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED; + } + v.setFastScrollFocusState(newState, animated); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 1f95133d49..0460c91066 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -24,12 +24,16 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.drawable.Drawable; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.net.Uri; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Recycler; +import android.support.v7.widget.RecyclerView.State; +import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -42,7 +46,6 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.util.Thunk; import java.util.HashMap; import java.util.List; @@ -69,6 +72,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter items = mApps.getAdapterItems(); + AlphabeticalAppsList.AdapterItem item = items.get(position); + if (item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_PREDICTION); + return; + } + } + } sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_ALL_APPS_A_Z); } @@ -212,63 +225,31 @@ public class AllAppsRecyclerView extends BaseRecyclerView List fastScrollSections = mApps.getFastScrollerSections(); AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); - if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { - for (int i = 1; i < fastScrollSections.size(); i++) { - AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); - if (info.touchFraction > touchFraction) { - break; - } - lastInfo = info; + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); + if (info.touchFraction > touchFraction) { + break; } - } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ - lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); - } else { - throw new RuntimeException("Unexpected scroll bar mode"); + lastInfo = info; } - // Map the touch position back to the scroll of the recycler view - getCurScrollState(mScrollPosState); - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); - if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); - } - - if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { - mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; - - // Reset the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } - - if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { - smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); - } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); - } - } else { - throw new RuntimeException("Unexpected fast scroll mode"); - } - } + // Update the fast scroll + int scrollY = getScrollTop(mScrollPosState); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); + mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo); return lastInfo.sectionName; } @Override public void onFastScrollCompleted() { super.onFastScrollCompleted(); - // Reset and clean up the last focused view - if (mLastFastScrollFocusedView != null) { - mLastFastScrollFocusedView.setFastScrollFocused(false, true); - mLastFastScrollFocusedView = null; - } - mPrevFastScrollFocusedPosition = -1; + mFastScrollHelper.onFastScrollCompleted(); + } + + @Override + public void setAdapter(Adapter adapter) { + super.setAdapter(adapter); + mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter); } /** @@ -286,7 +267,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Find the index and height of the first visible row (all rows have the same height) int rowCount = mApps.getNumAppRows(); - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); if (mScrollPosState.rowIndex < 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -294,7 +275,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Only show the scrollbar if there is height to be scrolled int availableScrollBarHeight = getAvailableScrollBarHeight(); - int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); if (availableScrollHeight <= 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -303,8 +284,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + - (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + int scrollY = getScrollTop(mScrollPosState); int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -354,59 +334,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } - /** - * This runnable runs a single frame of the smooth scroll animation and posts the next frame - * if necessary. - */ - @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { - @Override - public void run() { - if (mFastScrollFrameIndex < mFastScrollFrames.length) { - scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); - mFastScrollFrameIndex++; - postOnAnimation(mSmoothSnapNextFrameRunnable); - } else { - // Animation completed, set the fast scroll state on the target view - final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); - if (vh != null && - vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && - mLastFastScrollFocusedView != vh.itemView) { - mLastFastScrollFocusedView = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; - mLastFastScrollFocusedView.setFastScrollFocused(true, true); - } - } - } - }; - - /** - * Smoothly snaps to a given position. We do this manually by calculating the keyframes - * ourselves and animating the scroll on the recycler view. - */ - private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { - removeCallbacks(mSmoothSnapNextFrameRunnable); - - // Calculate the full animation from the current scroll position to the final scroll - // position, and then run the animation for the duration. - int curScrollY = getPaddingTop() + - (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; - int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); - int numFrames = mFastScrollFrames.length; - for (int i = 0; i < numFrames; i++) { - // TODO(winsonc): We can interpolate this as well. - mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; - } - mFastScrollFrameIndex = 0; - postOnAnimation(mSmoothSnapNextFrameRunnable); - } - /** * Returns the current scroll state of the apps rows. */ - protected void getCurScrollState(ScrollPositionState stateOut) { + protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; - stateOut.rowHeight = -1; + stateOut.itemPos = -1; // Return early if there are no items or we haven't been measured List items = mApps.getAdapterItems(); @@ -420,29 +354,31 @@ public class AllAppsRecyclerView extends BaseRecyclerView int position = getChildPosition(child); if (position != NO_POSITION) { AlphabeticalAppsList.AdapterItem item = items.get(position); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || - item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + if ((item.viewType & viewTypeMask) != 0) { stateOut.rowIndex = item.rowIndex; stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); - stateOut.rowHeight = child.getHeight(); - break; + stateOut.itemPos = position; + return; } } } + return; } - /** - * Returns the scrollY for the given position in the adapter. - */ - private int getScrollAtPosition(int position, int rowHeight) { - AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || - item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { - int offset = item.rowIndex > 0 ? getPaddingTop() : 0; - return offset + item.rowIndex * rowHeight; - } else { + @Override + protected boolean supportsFastScrolling() { + // Only allow fast scrolling when the user is not searching, since the results are not + // grouped in a meaningful order + return !mApps.hasFilter(); + } + + protected int getTop(int rowIndex) { + if (getChildCount() == 0 || rowIndex <= 0) { return 0; } + + // The prediction bar icons have more padding, so account for that in the row offset + return mPredictionIconHeight + (rowIndex - 1) * mIconHeight; } /** diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java index 2b363c0cbe..39d6dd50cd 100644 --- a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java +++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java @@ -15,11 +15,22 @@ */ package com.android.launcher3.allapps; -import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; import android.graphics.Rect; +import android.net.Uri; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; import android.view.View; -import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import com.android.launcher3.ExtendedEditText; +import com.android.launcher3.Launcher; +import com.android.launcher3.Utilities; import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; @@ -27,53 +38,134 @@ import java.util.ArrayList; /** * An interface to a search box that AllApps can command. */ -public abstract class AllAppsSearchBarController { +public abstract class AllAppsSearchBarController + implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener { + protected Launcher mLauncher; protected AlphabeticalAppsList mApps; protected Callbacks mCb; + protected ExtendedEditText mInput; + + protected DefaultAppSearchAlgorithm mSearchAlgorithm; + protected InputMethodManager mInputMethodManager; /** * Sets the references to the apps model and the search result callback. */ - public final void initialize(AlphabeticalAppsList apps, Callbacks cb) { + public final void initialize( + AlphabeticalAppsList apps, ExtendedEditText input, + Launcher launcher, Callbacks cb) { mApps = apps; mCb = cb; - onInitialize(); + mLauncher = launcher; + + mInput = input; + mInput.addTextChangedListener(this); + mInput.setOnEditorActionListener(this); + mInput.setOnBackKeyListener(this); + + mInputMethodManager = (InputMethodManager) + mInput.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + mSearchAlgorithm = onInitializeSearch(); } /** - * To be overridden by subclasses. This method will get called when the controller is set, - * before getView(). + * To be implemented by subclasses. This method will get called when the controller is set. */ - protected abstract void onInitialize(); + protected abstract DefaultAppSearchAlgorithm onInitializeSearch(); - /** - * Returns the search bar view. - * @param parent the parent to attach the search bar view to. - */ - public abstract View getView(ViewGroup parent); + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } - /** - * Focuses the search field to handle key events. - */ - public abstract void focusSearchField(); + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } - /** - * Returns whether the search field is focused. - */ - public abstract boolean isSearchFieldFocused(); + @Override + public void afterTextChanged(final Editable s) { + String query = s.toString(); + if (query.isEmpty()) { + mSearchAlgorithm.cancel(true); + mCb.clearSearchResult(); + } else { + mSearchAlgorithm.cancel(false); + mSearchAlgorithm.doSearch(query, mCb); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Skip if it's not the right action + if (actionId != EditorInfo.IME_ACTION_SEARCH) { + return false; + } + // Skip if the query is empty + String query = v.getText().toString(); + if (query.isEmpty()) { + return false; + } + return mLauncher.startActivitySafely(v, createMarketSearchIntent(query), null); + } + + @Override + public boolean onBackKey() { + // Only hide the search field if there is no query, or if there + // are no filtered results + String query = Utilities.trim(mInput.getEditableText().toString()); + if (query.isEmpty() || mApps.hasNoFilteredResults()) { + reset(); + return true; + } + return false; + } /** * Resets the search bar state. */ - public abstract void reset(); + public void reset() { + unfocusSearchField(); + mCb.clearSearchResult(); + mInput.setText(""); + mInputMethodManager.hideSoftInputFromWindow(mInput.getWindowToken(), 0); + } + + protected void unfocusSearchField() { + View nextFocus = mInput.focusSearch(View.FOCUS_DOWN); + if (nextFocus != null) { + nextFocus.requestFocus(); + } + } /** - * Returns whether the prediction bar should currently be visible depending on the state of - * the search bar. + * Focuses the search field to handle key events. */ - @Deprecated - public abstract boolean shouldShowPredictionBar(); + public void focusSearchField() { + mInput.requestFocus(); + mInputMethodManager.showSoftInput(mInput, InputMethodManager.SHOW_IMPLICIT); + } + + /** + * Returns whether the search field is focused. + */ + public boolean isSearchFieldFocused() { + return mInput.isFocused(); + } + + /** + * Creates a new market search intent. + */ + public Intent createMarketSearchIntent(String query) { + Uri marketSearchUri = Uri.parse("market://search") + .buildUpon() + .appendQueryParameter("c", "apps") + .appendQueryParameter("q", query) + .build(); + return new Intent(Intent.ACTION_VIEW).setData(marketSearchUri); + } /** * Callback for getting search results. diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index dac0df12a7..b533ce9d52 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -275,14 +275,21 @@ public class AlphabeticalAppsList { return (mSearchResults != null) && mFilteredApps.isEmpty(); } + public boolean hasPredictedComponents() { + return (mPredictedAppComponents != null && mPredictedAppComponents.size() > 0); + } + /** * Sets the sorted list of filtered components. */ - public void setOrderedFilter(ArrayList f) { + public boolean setOrderedFilter(ArrayList f) { if (mSearchResults != f) { + boolean same = mSearchResults != null && mSearchResults.equals(f); mSearchResults = f; updateAdapterItems(); + return !same; } + return false; } /** diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java index 3169f842a5..57747e3672 100644 --- a/src/com/android/launcher3/allapps/DefaultAppSearchController.java +++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java @@ -15,261 +15,12 @@ */ package com.android.launcher3.allapps; -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; -import com.android.launcher3.ExtendedEditText; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.Thunk; - -import java.util.List; - - /** * The default search controller. */ -final class DefaultAppSearchController extends AllAppsSearchBarController - implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener { +public class DefaultAppSearchController extends AllAppsSearchBarController { - private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; - - private static final int FADE_IN_DURATION = 175; - private static final int FADE_OUT_DURATION = 100; - private static final int SEARCH_TRANSLATION_X_DP = 18; - - private final Context mContext; - @Thunk final InputMethodManager mInputMethodManager; - - private DefaultAppSearchAlgorithm mSearchManager; - - private ViewGroup mContainerView; - private View mSearchView; - @Thunk View mSearchBarContainerView; - private View mSearchButtonView; - private View mDismissSearchButtonView; - @Thunk - ExtendedEditText mSearchBarEditView; - @Thunk AllAppsRecyclerView mAppsRecyclerView; - @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { - @Override - public void run() { - mAppsRecyclerView.requestFocus(); - } - }; - - public DefaultAppSearchController(Context context, ViewGroup containerView, - AllAppsRecyclerView appsRecyclerView) { - mContext = context; - mInputMethodManager = (InputMethodManager) - mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - mContainerView = containerView; - mAppsRecyclerView = appsRecyclerView; - } - - @Override - public View getView(ViewGroup parent) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false); - mSearchView.setOnClickListener(this); - - mSearchButtonView = mSearchView.findViewById(R.id.search_button); - mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); - mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); - mDismissSearchButtonView.setOnClickListener(this); - mSearchBarEditView = (ExtendedEditText) - mSearchBarContainerView.findViewById(R.id.search_box_input); - mSearchBarEditView.addTextChangedListener(this); - mSearchBarEditView.setOnEditorActionListener(this); - mSearchBarEditView.setOnBackKeyListener( - new ExtendedEditText.OnBackKeyListener() { - @Override - public boolean onBackKey() { - // Only hide the search field if there is no query, or if there - // are no filtered results - String query = Utilities.trim( - mSearchBarEditView.getEditableText().toString()); - if (query.isEmpty() || mApps.hasNoFilteredResults()) { - hideSearchField(true, mFocusRecyclerViewRunnable); - return true; - } - return false; - } - }); - return mSearchView; - } - - @Override - public void focusSearchField() { - mSearchBarEditView.requestFocus(); - showSearchField(); - } - - @Override - public boolean isSearchFieldFocused() { - return mSearchBarEditView.isFocused(); - } - - @Override - protected void onInitialize() { - mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps()); - } - - @Override - public void reset() { - hideSearchField(false, null); - } - - @Override - public boolean shouldShowPredictionBar() { - return false; - } - - @Override - public void onClick(View v) { - if (v == mSearchView) { - showSearchField(); - } else if (v == mDismissSearchButtonView) { - hideSearchField(true, mFocusRecyclerViewRunnable); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing - } - - @Override - public void afterTextChanged(final Editable s) { - String query = s.toString(); - if (query.isEmpty()) { - mSearchManager.cancel(true); - mCb.clearSearchResult(); - } else { - mSearchManager.cancel(false); - mSearchManager.doSearch(query, mCb); - } - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Skip if we disallow app-launch-on-enter - if (!ALLOW_SINGLE_APP_LAUNCH) { - return false; - } - // Skip if it's not the right action - if (actionId != EditorInfo.IME_ACTION_SEARCH) { - return false; - } - // Skip if there are more than one icon - if (mApps.getNumFilteredApps() > 1) { - return false; - } - // Otherwise, find the first icon, or fallback to the search-market-view and launch it - List items = mApps.getAdapterItems(); - for (int i = 0; i < items.size(); i++) { - AlphabeticalAppsList.AdapterItem item = items.get(i); - switch (item.viewType) { - case AllAppsGridAdapter.ICON_VIEW_TYPE: - case AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE: - mAppsRecyclerView.getChildAt(i).performClick(); - mInputMethodManager.hideSoftInputFromWindow( - mContainerView.getWindowToken(), 0); - return true; - } - } - return false; - } - - /** - * Focuses the search field. - */ - private void showSearchField() { - // Show the search bar and focus the search - final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, - mContext.getResources().getDisplayMetrics()); - mSearchBarContainerView.setVisibility(View.VISIBLE); - mSearchBarContainerView.setAlpha(0f); - mSearchBarContainerView.setTranslationX(translationX); - mSearchBarContainerView.animate() - .alpha(1f) - .translationX(0) - .setDuration(FADE_IN_DURATION) - .withLayer() - .withEndAction(new Runnable() { - @Override - public void run() { - mSearchBarEditView.requestFocus(); - mInputMethodManager.showSoftInput(mSearchBarEditView, - InputMethodManager.SHOW_IMPLICIT); - } - }); - mSearchButtonView.animate() - .alpha(0f) - .translationX(-translationX) - .setDuration(FADE_OUT_DURATION) - .withLayer(); - } - - /** - * Unfocuses the search field. - */ - @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) { - mSearchManager.cancel(true); - - final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; - final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, - mContext.getResources().getDisplayMetrics()); - if (animated) { - // Hide the search bar and focus the recycler view - mSearchBarContainerView.animate() - .alpha(0f) - .translationX(0) - .setDuration(FADE_IN_DURATION) - .withLayer() - .withEndAction(new Runnable() { - @Override - public void run() { - mSearchBarContainerView.setVisibility(View.INVISIBLE); - if (resetTextField) { - mSearchBarEditView.setText(""); - } - mCb.clearSearchResult(); - if (postAnimationRunnable != null) { - postAnimationRunnable.run(); - } - } - }); - mSearchButtonView.setTranslationX(-translationX); - mSearchButtonView.animate() - .alpha(1f) - .translationX(0) - .setDuration(FADE_OUT_DURATION) - .withLayer(); - } else { - mSearchBarContainerView.setVisibility(View.INVISIBLE); - if (resetTextField) { - mSearchBarEditView.setText(""); - } - mCb.clearSearchResult(); - mSearchButtonView.setAlpha(1f); - mSearchButtonView.setTranslationX(0f); - if (postAnimationRunnable != null) { - postAnimationRunnable.run(); - } - } - mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0); + public DefaultAppSearchAlgorithm onInitializeSearch() { + return new DefaultAppSearchAlgorithm(mApps.getApps()); } } diff --git a/src/com/android/launcher3/allapps/HeaderElevationController.java b/src/com/android/launcher3/allapps/HeaderElevationController.java new file mode 100644 index 0000000000..07f583caa8 --- /dev/null +++ b/src/com/android/launcher3/allapps/HeaderElevationController.java @@ -0,0 +1,101 @@ +package com.android.launcher3.allapps; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; + +import com.android.launcher3.R; + +/** + * Helper class for controlling the header elevation in response to RecyclerView scroll. + */ +public abstract class HeaderElevationController extends RecyclerView.OnScrollListener { + + private int mCurrentY = 0; + + public void reset() { + mCurrentY = 0; + onScroll(mCurrentY); + } + + @Override + public final void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mCurrentY += dy; + onScroll(mCurrentY); + } + + public void updateBackgroundPadding(Rect bgPadding) { } + + abstract void onScroll(int scrollY); + + public static class ControllerV16 extends HeaderElevationController { + + private final View mShadow; + private final float mScrollToElevation; + + public ControllerV16(View header) { + Resources res = header.getContext().getResources(); + mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation); + + mShadow = new View(header.getContext()); + mShadow.setBackground(new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, new int[] {0x1E000000, 0x00000000})); + mShadow.setAlpha(0); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + res.getDimensionPixelSize(R.dimen.all_apps_header_shadow_height)); + lp.topMargin = ((FrameLayout.LayoutParams) header.getLayoutParams()).height; + + ((ViewGroup) header.getParent()).addView(mShadow, lp); + } + + @Override + public void onScroll(int scrollY) { + float elevationPct = (float) Math.min(scrollY, mScrollToElevation) / + mScrollToElevation; + mShadow.setAlpha(elevationPct); + } + + @Override + public void updateBackgroundPadding(Rect bgPadding) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mShadow.getLayoutParams(); + lp.leftMargin = bgPadding.left; + lp.rightMargin = bgPadding.right; + mShadow.requestLayout(); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static class ControllerVL extends HeaderElevationController { + + private final View mHeader; + private final float mMaxElevation; + private final float mScrollToElevation; + + public ControllerVL(View header) { + mHeader = header; + mHeader.setOutlineProvider(ViewOutlineProvider.BOUNDS); + + Resources res = header.getContext().getResources(); + mMaxElevation = res.getDimension(R.dimen.all_apps_header_max_elevation); + mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation); + } + + @Override + public void onScroll(int scrollY) { + float elevationPct = Math.min(scrollY, mScrollToElevation) / mScrollToElevation; + float newElevation = mMaxElevation * elevationPct; + if (Float.compare(mHeader.getElevation(), newElevation) != 0) { + mHeader.setElevation(newElevation); + } + } + } +} diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java index 434f13dcc1..811cacfc09 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; @@ -28,7 +29,9 @@ import android.os.Bundle; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; +import com.android.launcher3.util.ComponentKey; +import java.util.HashMap; import java.util.List; public abstract class AppWidgetManagerCompat { @@ -62,6 +65,11 @@ public abstract class AppWidgetManagerCompat { return mAppWidgetManager.getAppWidgetInfo(appWidgetId); } + public LauncherAppWidgetProviderInfo getLauncherAppWidgetInfo(int appWidgetId) { + AppWidgetProviderInfo info = getAppWidgetInfo(appWidgetId); + return info == null ? null : LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info); + } + public abstract List getAllProviders(); public abstract String loadLabel(LauncherAppWidgetProviderInfo info); @@ -79,6 +87,10 @@ public abstract class AppWidgetManagerCompat { public abstract Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache); public abstract Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, - int imageHeight); + int imageWidth, int imageHeight); + public abstract LauncherAppWidgetProviderInfo findProvider( + ComponentName provider, UserHandleCompat user); + + public abstract HashMap getAllProvidersMap(); } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java index 463cf902d1..de9414ed27 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -31,7 +32,9 @@ import android.os.Bundle; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; +import com.android.launcher3.util.ComponentKey; +import java.util.HashMap; import java.util.List; class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { @@ -88,7 +91,28 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { @Override public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, - int imageHeight) { + int imageWidth, int imageHeight) { return bitmap; } + + @Override + public LauncherAppWidgetProviderInfo findProvider( + ComponentName provider, UserHandleCompat user) { + for (AppWidgetProviderInfo info : mAppWidgetManager.getInstalledProviders()) { + if (info.provider.equals(provider)) { + return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info); + } + } + return null; + } + + @Override + public HashMap getAllProvidersMap() { + HashMap result = new HashMap<>(); + UserHandleCompat user = UserHandleCompat.myUserHandle(); + for (AppWidgetProviderInfo info : mAppWidgetManager.getInstalledProviders()) { + result.put(new ComponentKey(info.provider, user), info); + } + return result; + } } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java index 13712d8c71..a1570e66a1 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -40,8 +41,10 @@ import android.widget.Toast; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.R; +import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -90,9 +93,7 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { AppWidgetHost host, int requestCode) { try { host.startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, null); - } catch (ActivityNotFoundException e) { - Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - } catch (SecurityException e) { + } catch (ActivityNotFoundException | SecurityException e) { Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); } } @@ -109,18 +110,24 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { @Override public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, - int imageHeight) { + int imageWidth, int imageHeight) { if (info.isCustomWidget || info.getProfile().equals(android.os.Process.myUserHandle())) { return bitmap; } // Add a user badge in the bottom right of the image. final Resources res = mContext.getResources(); - final int badgeSize = res.getDimensionPixelSize(R.dimen.profile_badge_size); final int badgeMinTop = res.getDimensionPixelSize(R.dimen.profile_badge_minimum_top); + + // choose min between badge size defined for widget tray versus width, height of the image. + // Width, height of the image can be smaller than widget tray badge size when being dropped + // to the workspace. + final int badgeSize = Math.min(res.getDimensionPixelSize(R.dimen.profile_badge_size), + Math.min(imageWidth, imageHeight - badgeMinTop)); final Rect badgeLocation = new Rect(0, 0, badgeSize, badgeSize); final int top = Math.max(imageHeight - badgeSize, badgeMinTop); + if (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { badgeLocation.offset(0, top); } else { @@ -141,4 +148,28 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { c.setBitmap(null); return bitmap; } + + @Override + public LauncherAppWidgetProviderInfo findProvider(ComponentName provider, UserHandleCompat user) { + for (AppWidgetProviderInfo info : mAppWidgetManager + .getInstalledProvidersForProfile(user.getUser())) { + if (info.provider.equals(provider)) { + return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info); + } + } + return null; + } + + @Override + public HashMap getAllProvidersMap() { + HashMap result = new HashMap<>(); + for (UserHandle user : mUserManager.getUserProfiles()) { + UserHandleCompat userHandle = UserHandleCompat.fromUser(user); + for (AppWidgetProviderInfo info : + mAppWidgetManager.getInstalledProvidersForProfile(user)) { + result.put(new ComponentKey(info.provider, userHandle), info); + } + } + return result; + } } diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java index 07ef0efb71..0bc9588aae 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java @@ -33,7 +33,6 @@ public abstract class LauncherActivityInfoCompat { public abstract Drawable getIcon(int density); public abstract ApplicationInfo getApplicationInfo(); public abstract long getFirstInstallTime(); - public abstract Drawable getBadgedIcon(int density); /** * Creates a LauncherActivityInfoCompat for the primary user. diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java index ea51aace87..fee0376bd3 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java @@ -93,8 +93,4 @@ public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { public String getName() { return mActivityInfo.name; } - - public Drawable getBadgedIcon(int density) { - return getIcon(density); - } } diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java index 4448758e7a..67c5c27951 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java @@ -55,8 +55,4 @@ public class LauncherActivityInfoCompatVL extends LauncherActivityInfoCompat { public long getFirstInstallTime() { return mLauncherActivityInfo.getFirstInstallTime(); } - - public Drawable getBadgedIcon(int density) { - return mLauncherActivityInfo.getBadgedIcon(density); - } } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java index 95e3ba9024..237a9e9fb8 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompat.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java @@ -19,9 +19,6 @@ package com.android.launcher3.compat; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Rect; import android.os.Bundle; @@ -35,6 +32,10 @@ public abstract class LauncherAppsCompat { "android.intent.action.MANAGED_PROFILE_ADDED"; public static final String ACTION_MANAGED_PROFILE_REMOVED = "android.intent.action.MANAGED_PROFILE_REMOVED"; + public static final String ACTION_MANAGED_PROFILE_AVAILABLE = + "android.intent.action.MANAGED_PROFILE_AVAILABLE"; + public static final String ACTION_MANAGED_PROFILE_UNAVAILABLE = + "android.intent.action.MANAGED_PROFILE_UNAVAILABLE"; public interface OnAppsChangedCallbackCompat { void onPackageRemoved(String packageName, UserHandleCompat user); @@ -42,6 +43,8 @@ public abstract class LauncherAppsCompat { void onPackageChanged(String packageName, UserHandleCompat user); void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing); void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing); + void onPackagesSuspended(String[] packageNames, UserHandleCompat user); + void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user); } protected LauncherAppsCompat() { @@ -75,13 +78,5 @@ public abstract class LauncherAppsCompat { public abstract boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user); public abstract boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user); - - public boolean isAppEnabled(PackageManager pm, String packageName, int flags) { - try { - ApplicationInfo info = pm.getApplicationInfo(packageName, flags); - return info != null && info.enabled; - } catch (NameNotFoundException e) { - return false; - } - } -} \ No newline at end of file + public abstract boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user); +} diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index 339c457e18..4e2fc055e0 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -27,11 +27,11 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import com.android.launcher3.Utilities; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -114,7 +114,7 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { } public boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user) { - return isAppEnabled(mPm, packageName, 0); + return PackageManagerHelper.isAppEnabled(mPm, packageName); } public boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user) { @@ -126,6 +126,10 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { } } + public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) { + return false; + } + private void unregisterForPackageIntents() { mContext.unregisterReceiver(mPackageMonitor); } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java index fbf91b548c..7270d023be 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -36,7 +36,7 @@ import java.util.Map; @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class LauncherAppsCompatVL extends LauncherAppsCompat { - private LauncherApps mLauncherApps; + protected LauncherApps mLauncherApps; private Map mCallbacks = new HashMap(); @@ -106,6 +106,10 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat { return mLauncherApps.isActivityEnabled(component, user.getUser()); } + public boolean isPackageSuspendedForProfile(String packageName, UserHandleCompat user) { + return false; + } + private static class WrappedCallback extends LauncherApps.Callback { private LauncherAppsCompat.OnAppsChangedCallbackCompat mCallback; @@ -134,6 +138,14 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat { mCallback.onPackagesUnavailable(packageNames, UserHandleCompat.fromUser(user), replacing); } + + public void onPackagesSuspended(String[] packageNames, UserHandle user) { + mCallback.onPackagesSuspended(packageNames, UserHandleCompat.fromUser(user)); + } + + public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { + mCallback.onPackagesUnsuspended(packageNames, UserHandleCompat.fromUser(user)); + } } } diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java index 3ad51017d2..4aa667ed09 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java @@ -43,7 +43,6 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat { PackageInstallerCompatVL(Context context) { mInstaller = context.getPackageManager().getPackageInstaller(); - LauncherAppState.setApplicationContext(context.getApplicationContext()); mCache = LauncherAppState.getInstance().getIconCache(); mWorker = new Handler(LauncherModel.getWorkerLooper()); @@ -107,7 +106,7 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat { @Override public void onProgressChanged(int sessionId, float progress) { SessionInfo session = mInstaller.getSessionInfo(sessionId); - if (session != null) { + if (session != null && session.getAppPackageName() != null) { sendUpdate(new PackageInstallInfo(session.getAppPackageName(), STATUS_INSTALLING, (int) (session.getProgress() * 100))); @@ -124,7 +123,7 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat { private void pushSessionDisplayToLauncher(int sessionId) { SessionInfo session = mInstaller.getSessionInfo(sessionId); - if (session != null) { + if (session != null && session.getAppPackageName() != null) { addSessionInfoToCahce(session, UserHandleCompat.myUserHandle()); LauncherAppState app = LauncherAppState.getInstanceNoCreate(); diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java index 567022b437..50af21b968 100644 --- a/src/com/android/launcher3/compat/UserHandleCompat.java +++ b/src/com/android/launcher3/compat/UserHandleCompat.java @@ -49,7 +49,7 @@ public class UserHandleCompat { } } - UserHandle getUser() { + public UserHandle getUser() { return mUser; } @@ -93,4 +93,14 @@ public class UserHandleCompat { intent.putExtra(name, mUser); } } + + public static UserHandleCompat fromIntent(Intent intent) { + if (Utilities.ATLEAST_LOLLIPOP) { + UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); + if (user != null) { + return UserHandleCompat.fromUser(user); + } + } + return null; + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java index f708004a33..978f9229da 100644 --- a/src/com/android/launcher3/compat/UserManagerCompat.java +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -17,8 +17,6 @@ package com.android.launcher3.compat; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Build; import com.android.launcher3.Utilities; @@ -34,7 +32,9 @@ public abstract class UserManagerCompat { public static UserManagerCompat getInstance(Context context) { synchronized (sInstanceLock) { if (sInstance == null) { - if (Utilities.ATLEAST_LOLLIPOP) { + if (Utilities.isNycOrAbove()) { + sInstance = new UserManagerCompatVN(context.getApplicationContext()); + } else if (Utilities.ATLEAST_LOLLIPOP) { sInstance = new UserManagerCompatVL(context.getApplicationContext()); } else if (Utilities.ATLEAST_JB_MR1) { sInstance = new UserManagerCompatV17(context.getApplicationContext()); @@ -54,7 +54,7 @@ public abstract class UserManagerCompat { public abstract List getUserProfiles(); public abstract long getSerialNumberForUser(UserHandleCompat user); public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); - public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user); public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user); public abstract long getUserCreationTime(UserHandleCompat user); + public abstract boolean isQuietModeEnabled(UserHandleCompat user); } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java index 85aee57e85..a006efd50d 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV16.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -16,8 +16,6 @@ package com.android.launcher3.compat; -import android.graphics.drawable.Drawable; - import java.util.ArrayList; import java.util.List; @@ -36,11 +34,6 @@ public class UserManagerCompatV16 extends UserManagerCompat { return UserHandleCompat.myUserHandle(); } - public Drawable getBadgedDrawableForUser(Drawable unbadged, - UserHandleCompat user) { - return unbadged; - } - public long getSerialNumberForUser(UserHandleCompat user) { return 0; } @@ -57,4 +50,9 @@ public class UserManagerCompatV16 extends UserManagerCompat { @Override public void enableAndResetCache() { } + + @Override + public boolean isQuietModeEnabled(UserHandleCompat user) { + return false; + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index dc3ec3cd85..c53d702b76 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -21,11 +21,9 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; import android.os.Build; import android.os.UserHandle; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.Utilities; import com.android.launcher3.util.LongArrayMap; @@ -86,11 +84,6 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { return compatUsers; } - @Override - public Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user) { - return mPm.getUserBadgedIcon(unbadged, user.getUser()); - } - @Override public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { if (user == null) { @@ -104,8 +97,7 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { if (Utilities.ATLEAST_MARSHMALLOW) { return mUserManager.getUserCreationTime(user.getUser()); } - SharedPreferences prefs = mContext.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + SharedPreferences prefs = Utilities.getPrefs(mContext); String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user); if (!prefs.contains(key)) { prefs.edit().putLong(key, System.currentTimeMillis()).apply(); diff --git a/src/com/android/launcher3/compat/UserManagerCompatVN.java b/src/com/android/launcher3/compat/UserManagerCompatVN.java new file mode 100644 index 0000000000..ae41e68a36 --- /dev/null +++ b/src/com/android/launcher3/compat/UserManagerCompatVN.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compat; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +//TODO: Once gogole3 SDK is updated to N, add @TargetApi(Build.VERSION_CODES.N) +public class UserManagerCompatVN extends UserManagerCompatVL { + + private static final String TAG = "UserManagerCompatVN"; + + UserManagerCompatVN(Context context) { + super(context); + } + + @Override + public boolean isQuietModeEnabled(UserHandleCompat user) { + if (user != null) { + try { + //TODO: Replace with proper API call once google3 SDK is updated. + Method isQuietModeEnabledMethod = UserManager.class.getMethod("isQuietModeEnabled", + UserHandle.class); + return (boolean) isQuietModeEnabledMethod.invoke(mUserManager, user.getUser()); + } catch (NoSuchMethodError | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + Log.e(TAG, "Running on N without isQuietModeEnabled", e); + } catch (IllegalArgumentException e) { + // TODO remove this when API is fixed to not throw this + // when called on user that isn't a managed profile. + } + } + return false; + } +} + diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java new file mode 100644 index 0000000000..50127085a2 --- /dev/null +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.config; + +/** + * Defines a set of flags used to control various launcher behaviors + * All the flags must be defined as + * public static boolean LAUNCHER3_FLAG_NAME = true/false; + * + * Use LAUNCHER3_ prefix for prevent namespace conflicts. + */ +public final class FeatureFlags { + + private FeatureFlags() {} + + public static boolean IS_DEV_BUILD = false; + public static boolean IS_ALPHA_BUILD = false; + public static boolean IS_RELEASE_BUILD = true; + + // Custom flags go below this + public static boolean LAUNCHER3_ICON_NORMALIZATION = false; + +} diff --git a/src/com/android/launcher3/logging/UserEventLogger.java b/src/com/android/launcher3/logging/UserEventLogger.java new file mode 100644 index 0000000000..d05b683065 --- /dev/null +++ b/src/com/android/launcher3/logging/UserEventLogger.java @@ -0,0 +1,4 @@ +package com.android.launcher3.logging; + +public abstract class UserEventLogger { +} \ No newline at end of file diff --git a/src/com/android/launcher3/model/MigrateFromRestoreTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java similarity index 54% rename from src/com/android/launcher3/model/MigrateFromRestoreTask.java rename to src/com/android/launcher3/model/GridSizeMigrationTask.java index 6a529f61f7..8117122055 100644 --- a/src/com/android/launcher3/model/MigrateFromRestoreTask.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.database.Cursor; import android.graphics.Point; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -21,32 +22,37 @@ import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; +import com.android.launcher3.backup.nano.BackupProtos; +import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.util.LongArrayMap; -import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; /** * This class takes care of shrinking the workspace (by maximum of one row and one column), as a - * result of restoring from a larger device. + * result of restoring from a larger device or device density change. */ -public class MigrateFromRestoreTask { +public class GridSizeMigrationTask { - public static boolean ENABLED = false; + public static boolean ENABLED = Utilities.isNycOrAbove(); - private static final String TAG = "MigrateFromRestoreTask"; + private static final String TAG = "GridSizeMigrationTask"; private static final boolean DEBUG = true; - private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size"; + private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size"; + private static final String KEY_MIGRATION_SRC_HOTSEAT_SIZE = "migration_src_hotseat_size"; + + // Set of entries indicating minimum size a widget can be resized to. This is used during + // restore in case the widget has not been installed yet. private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size"; // These are carefully selected weights for various item types (Math.random?), to allow for - // the lease absurd migration experience. + // the least absurd migration experience. private static final float WT_SHORTCUT = 1; private static final float WT_APPLICATION = 0.8f; private static final float WT_WIDGET_MIN = 2; @@ -54,59 +60,141 @@ public class MigrateFromRestoreTask { private static final float WT_FOLDER_FACTOR = 0.5f; private final Context mContext; - private final ContentValues mTempValues = new ContentValues(); - private final HashMap mWidgetMinSize; private final InvariantDeviceProfile mIdp; - private HashSet mValidPackages; - public ArrayList mEntryToRemove; - private ArrayList mUpdateOperations; - - private ArrayList mCarryOver; + private final HashMap mWidgetMinSize = new HashMap<>(); + private final ContentValues mTempValues = new ContentValues(); + private final ArrayList mEntryToRemove = new ArrayList<>(); + private final ArrayList mUpdateOperations = new ArrayList<>(); + private final ArrayList mCarryOver = new ArrayList<>(); + private final HashSet mValidPackages; private final int mSrcX, mSrcY; - @Thunk final int mTrgX, mTrgY; + private final int mTrgX, mTrgY; private final boolean mShouldRemoveX, mShouldRemoveY; - public MigrateFromRestoreTask(Context context) { - mContext = context; + private final int mSrcHotseatSize; + private final int mSrcAllAppsRank; + private final int mDestHotseatSize; + private final int mDestAllAppsRank; + + protected GridSizeMigrationTask(Context context, InvariantDeviceProfile idp, + HashSet validPackages, HashMap widgetMinSize, + Point sourceSize, Point targetSize) { + mContext = context; + mValidPackages = validPackages; + mWidgetMinSize.putAll(widgetMinSize); + mIdp = idp; - SharedPreferences prefs = prefs(context); - Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, "")); mSrcX = sourceSize.x; mSrcY = sourceSize.y; - mWidgetMinSize = new HashMap(); - for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, - Collections.emptySet())) { - String[] parts = s.split("#"); - mWidgetMinSize.put(parts[0], parsePoint(parts[1])); - } + mTrgX = targetSize.x; + mTrgY = targetSize.y; - mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); - mTrgX = mIdp.numColumns; - mTrgY = mIdp.numRows; mShouldRemoveX = mTrgX < mSrcX; mShouldRemoveY = mTrgY < mSrcY; + + // Non-used variables + mSrcHotseatSize = mSrcAllAppsRank = mDestHotseatSize = mDestAllAppsRank = -1; } - public void execute() throws Exception { - mEntryToRemove = new ArrayList<>(); - mCarryOver = new ArrayList<>(); - mUpdateOperations = new ArrayList<>(); + protected GridSizeMigrationTask(Context context, + InvariantDeviceProfile idp, HashSet validPackages, + int srcHotseatSize, int srcAllAppsRank, + int destHotseatSize, int destAllAppsRank) { + mContext = context; + mIdp = idp; + mValidPackages = validPackages; - // Initialize list of valid packages. This contain all the packages which are already on - // the device and packages which are being installed. Any item which doesn't belong to - // this set is removed. - // Since the loader removes such items anyway, removing these items here doesn't cause any - // extra data loss and gives us more free space on the grid for better migration. - mValidPackages = new HashSet<>(); - for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) { - mValidPackages.add(info.packageName); + mSrcHotseatSize = srcHotseatSize; + mSrcAllAppsRank = srcAllAppsRank; + + mDestHotseatSize = destHotseatSize; + mDestAllAppsRank = destAllAppsRank; + + // Non-used variables + mSrcX = mSrcY = mTrgX = mTrgY = -1; + mShouldRemoveX = mShouldRemoveY = false; + } + + /** + * Applied all the pending DB operations + * @return true if any DB operation was commited. + */ + private boolean applyOperations() throws Exception { + // Update items + if (!mUpdateOperations.isEmpty()) { + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); } - mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext) - .updateAndGetActiveSessionCache().keySet()); + if (!mEntryToRemove.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + } + mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, mEntryToRemove), null); + } + + return !mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty(); + } + + /** + * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them + * in the order in the new hotseat while keeping an empty space for all-apps. If the number of + * entries is more than what can fit in the new hotseat, we drop the entries with least weight. + * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION} + * & {@see #WT_FOLDER_FACTOR}. + * @return true if any DB change was made + */ + protected boolean migrateHotseat() throws Exception { + ArrayList items = loadHotseatEntries(); + + int requiredCount = mDestHotseatSize - 1; + + while (items.size() > requiredCount) { + // Pick the center item by default. + DbEntry toRemove = items.get(items.size() / 2); + + // Find the item with least weight. + for (DbEntry entry : items) { + if (entry.weight < toRemove.weight) { + toRemove = entry; + } + } + + mEntryToRemove.add(toRemove.id); + items.remove(toRemove); + } + + // Update screen IDS + int newScreenId = 0; + for (DbEntry entry : items) { + if (entry.screenId != newScreenId) { + entry.screenId = newScreenId; + + // These values does not affect the item position, but we should set them + // to something other than -1. + entry.cellX = newScreenId; + entry.cellY = 0; + + update(entry); + } + + newScreenId++; + if (newScreenId == mDestAllAppsRank) { + newScreenId++; + } + } + + return applyOperations(); + } + + /** + * @return true if any DB change was made + */ + protected boolean migrateWorkspace() throws Exception { ArrayList allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); if (allScreens.isEmpty()) { throw new Exception("Unable to get workspace screens"); @@ -150,31 +238,19 @@ public class MigrateFromRestoreTask { } while (!mCarryOver.isEmpty()); - - LauncherAppState.getInstance().getModel() - .updateWorkspaceScreenOrder(mContext, allScreens); - } - - // Update items - mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); - - if (!mEntryToRemove.isEmpty()) { - if (DEBUG) { - Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + // Update screens + final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; + mUpdateOperations.add(ContentProviderOperation.newDelete(uri).build()); + int count = allScreens.size(); + for (int i = 0; i < count; i++) { + ContentValues v = new ContentValues(); + long screenId = allScreens.get(i); + v.put(LauncherSettings.WorkspaceScreens._ID, screenId); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(v).build()); } - mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, - Utilities.createDbSelectionQuery( - LauncherSettings.Favorites._ID, mEntryToRemove), null); - } - - // Make sure we haven't removed everything. - final Cursor c = mContext.getContentResolver().query( - LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); - boolean hasData = c.moveToNext(); - c.close(); - if (!hasData) { - throw new Exception("Removed every thing during grid resize"); } + return applyOperations(); } /** @@ -188,7 +264,7 @@ public class MigrateFromRestoreTask { * (otherwise they are placed on a new screen). */ private void migrateScreen(long screenId) { - ArrayList items = loadEntries(screenId); + ArrayList items = loadWorkspaceEntries(screenId); int removedCol = Integer.MAX_VALUE; int removedRow = Integer.MAX_VALUE; @@ -326,7 +402,7 @@ public class MigrateFromRestoreTask { return finalItems; } - @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) { + private void markCells(boolean[][] occupied, DbEntry item, boolean val) { for (int i = item.cellX; i < (item.cellX + item.spanX); i++) { for (int j = item.cellY; j < (item.cellY + item.spanY); j++) { occupied[i][j] = val; @@ -334,7 +410,7 @@ public class MigrateFromRestoreTask { } } - @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { + private boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { if (x + w > mTrgX) return false; if (y + h > mTrgY) return false; @@ -542,100 +618,163 @@ public class MigrateFromRestoreTask { } } + private ArrayList loadHotseatEntries() { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{ + Favorites._ID, // 0 + Favorites.ITEM_TYPE, // 1 + Favorites.INTENT, // 2 + Favorites.SCREEN}, // 3 + Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null); + + final int indexId = c.getColumnIndexOrThrow(Favorites._ID); + final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); + final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN); + + ArrayList entries = new ArrayList<>(); + while (c.moveToNext()) { + DbEntry entry = new DbEntry(); + entry.id = c.getLong(indexId); + entry.itemType = c.getInt(indexItemType); + entry.screenId = c.getLong(indexScreen); + + if (entry.screenId >= mSrcHotseatSize) { + mEntryToRemove.add(entry.id); + continue; + } + + try { + // calculate weight + switch (entry.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + verifyIntent(c.getString(indexIntent)); + entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT + ? WT_SHORTCUT : WT_APPLICATION; + break; + } + case Favorites.ITEM_TYPE_FOLDER: { + int total = getFolderItemsCount(entry.id); + if (total == 0) { + throw new Exception("Folder is empty"); + } + entry.weight = WT_FOLDER_FACTOR * total; + break; + } + default: + throw new Exception("Invalid item type"); + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Removing item " + entry.id, e); + } + mEntryToRemove.add(entry.id); + continue; + } + entries.add(entry); + } + c.close(); + return entries; + } + + /** * Loads entries for a particular screen id. */ - public ArrayList loadEntries(long screen) { - Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { - Favorites._ID, // 0 - Favorites.ITEM_TYPE, // 1 - Favorites.CELLX, // 2 - Favorites.CELLY, // 3 - Favorites.SPANX, // 4 - Favorites.SPANY, // 5 - Favorites.INTENT, // 6 - Favorites.APPWIDGET_PROVIDER}, // 7 + private ArrayList loadWorkspaceEntries(long screen) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{ + Favorites._ID, // 0 + Favorites.ITEM_TYPE, // 1 + Favorites.CELLX, // 2 + Favorites.CELLY, // 3 + Favorites.SPANX, // 4 + Favorites.SPANY, // 5 + Favorites.INTENT, // 6 + Favorites.APPWIDGET_PROVIDER, // 7 + Favorites.APPWIDGET_ID}, // 8 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP - + " AND " + Favorites.SCREEN + " = " + screen, null, null, null); + + " AND " + Favorites.SCREEN + " = " + screen, null, null, null); - final int indexId = c.getColumnIndexOrThrow(Favorites._ID); - final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); - final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); - final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); - final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); - final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); - final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); - final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + final int indexId = c.getColumnIndexOrThrow(Favorites._ID); + final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); + final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); + final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); + final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); + final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); + final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID); - ArrayList entries = new ArrayList<>(); - while (c.moveToNext()) { - DbEntry entry = new DbEntry(); - entry.id = c.getLong(indexId); - entry.itemType = c.getInt(indexItemType); - entry.cellX = c.getInt(indexCellX); - entry.cellY = c.getInt(indexCellY); - entry.spanX = c.getInt(indexSpanX); - entry.spanY = c.getInt(indexSpanY); - entry.screenId = screen; + ArrayList entries = new ArrayList<>(); + while (c.moveToNext()) { + DbEntry entry = new DbEntry(); + entry.id = c.getLong(indexId); + entry.itemType = c.getInt(indexItemType); + entry.cellX = c.getInt(indexCellX); + entry.cellY = c.getInt(indexCellY); + entry.spanX = c.getInt(indexSpanX); + entry.spanY = c.getInt(indexSpanY); + entry.screenId = screen; - try { - // calculate weight - switch (entry.itemType) { - case Favorites.ITEM_TYPE_SHORTCUT: - case Favorites.ITEM_TYPE_APPLICATION: { - verifyIntent(c.getString(indexIntent)); - entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT - ? WT_SHORTCUT : WT_APPLICATION; - break; - } - case Favorites.ITEM_TYPE_APPWIDGET: { - String provider = c.getString(indexAppWidgetProvider); - ComponentName cn = ComponentName.unflattenFromString(provider); - verifyPackage(cn.getPackageName()); - entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR - * entry.spanX * entry.spanY); + try { + // calculate weight + switch (entry.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + verifyIntent(c.getString(indexIntent)); + entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT + ? WT_SHORTCUT : WT_APPLICATION; + break; + } + case Favorites.ITEM_TYPE_APPWIDGET: { + String provider = c.getString(indexAppWidgetProvider); + ComponentName cn = ComponentName.unflattenFromString(provider); + verifyPackage(cn.getPackageName()); + entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR + * entry.spanX * entry.spanY); - // Migration happens for current user only. - LauncherAppWidgetProviderInfo pInfo = LauncherModel.getProviderInfo( - mContext, cn, UserHandleCompat.myUserHandle()); - Point spans = pInfo == null ? - mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext); - if (spans != null) { - entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; - entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; - } else { - // Assume that the widget be resized down to 2x2 - entry.minSpanX = entry.minSpanY = 2; - } + int widgetId = c.getInt(indexAppWidgetId); + LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance( + mContext).getLauncherAppWidgetInfo(widgetId); + Point spans = pInfo == null ? + mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext); + if (spans != null) { + entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; + entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; + } else { + // Assume that the widget be resized down to 2x2 + entry.minSpanX = entry.minSpanY = 2; + } - if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { - throw new Exception("Widget can't be resized down to fit the grid"); - } - break; - } - case Favorites.ITEM_TYPE_FOLDER: { - int total = getFolderItemsCount(entry.id); - if (total == 0) { - throw new Exception("Folder is empty"); - } - entry.weight = WT_FOLDER_FACTOR * total; - break; - } - default: - throw new Exception("Invalid item type"); - } - } catch (Exception e) { - if (DEBUG) { - Log.d(TAG, "Removing item " + entry.id, e); - } - mEntryToRemove.add(entry.id); - continue; - } - - entries.add(entry); - } - return entries; + if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { + throw new Exception("Widget can't be resized down to fit the grid"); + } + break; + } + case Favorites.ITEM_TYPE_FOLDER: { + int total = getFolderItemsCount(entry.id); + if (total == 0) { + throw new Exception("Folder is empty"); + } + entry.weight = WT_FOLDER_FACTOR * total; + break; + } + default: + throw new Exception("Invalid item type"); + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Removing item " + entry.id, e); + } + mEntryToRemove.add(entry.id); + continue; + } + entries.add(entry); + } + c.close(); + return entries; } /** @@ -643,7 +782,7 @@ public class MigrateFromRestoreTask { */ private int getFolderItemsCount(long folderId) { Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, - new String[] {Favorites._ID, Favorites.INTENT}, + new String[]{Favorites._ID, Favorites.INTENT}, Favorites.CONTAINER + " = " + folderId, null, null, null); int total = 0; @@ -655,7 +794,7 @@ public class MigrateFromRestoreTask { mEntryToRemove.add(c.getLong(0)); } } - + c.close(); return total; } @@ -730,7 +869,7 @@ public class MigrateFromRestoreTask { } } - @Thunk static ArrayList deepCopy(ArrayList src) { + private static ArrayList deepCopy(ArrayList src) { ArrayList dup = new ArrayList(src.size()); for (DbEntry e : src) { dup.add(e.copy()); @@ -743,25 +882,149 @@ public class MigrateFromRestoreTask { return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1])); } - public static void markForMigration(Context context, int srcX, int srcY, - HashSet widgets) { - prefs(context).edit() - .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY) + private static String getPointString(int x, int y) { + return String.format(Locale.ENGLISH, "%d,%d", x, y); + } + + public static void markForMigration( + Context context, HashSet widgets, BackupProtos.DeviceProfieData srcProfile) { + Utilities.getPrefs(context).edit() + .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, + getPointString((int) srcProfile.desktopCols, (int) srcProfile.desktopRows)) + .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, + getPointString((int) srcProfile.hotseatCount, srcProfile.allappsRank)) .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets) .apply(); } - public static boolean shouldRunTask(Context context) { - return !TextUtils.isEmpty(prefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, "")); - } + /** + * Migrates the workspace and hotseat in case their sizes changed. + * @return false if the migration failed. + */ + public static boolean migrateGridIfNeeded(Context context) { + SharedPreferences prefs = Utilities.getPrefs(context); + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); - public static void clearFlags(Context context) { - prefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE) - .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); - } + String gridSizeString = getPointString(idp.numColumns, idp.numRows); + String hotseatSizeString = getPointString(idp.numHotseatIcons, idp.hotseatAllAppsRank); - private static SharedPreferences prefs(Context context) { - return context.getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), - Context.MODE_PRIVATE); + if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) && + hotseatSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, ""))) { + // Skip if workspace and hotseat sizes have not changed. + return true; + } + + long migrationStartTime = System.currentTimeMillis(); + try { + boolean dbChanged = false; + + // Initialize list of valid packages. This contain all the packages which are already on + // the device and packages which are being installed. Any item which doesn't belong to + // this set is removed. + // Since the loader removes such items anyway, removing these items here doesn't cause + // any extra data loss and gives us more free space on the grid for better migration. + HashSet validPackages = new HashSet<>(); + for (PackageInfo info : context.getPackageManager().getInstalledPackages(0)) { + validPackages.add(info.packageName); + } + validPackages.addAll(PackageInstallerCompat.getInstance(context) + .updateAndGetActiveSessionCache().keySet()); + + // Hotseat + Point srcHotseatSize = parsePoint(prefs.getString( + KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString)); + if (srcHotseatSize.x != idp.numHotseatIcons || + srcHotseatSize.y != idp.hotseatAllAppsRank) { + // Migrate hotseat. + + dbChanged = new GridSizeMigrationTask(context, + LauncherAppState.getInstance().getInvariantDeviceProfile(), + validPackages, + srcHotseatSize.x, srcHotseatSize.y, + idp.numHotseatIcons, idp.hotseatAllAppsRank).migrateHotseat(); + } + + // Grid size + Point targetSize = new Point(idp.numColumns, idp.numRows); + Point sourceSize = parsePoint(prefs.getString( + KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)); + + if (!targetSize.equals(sourceSize)) { + + // The following list defines all possible grid sizes (and intermediate steps + // during migration). Note that at each step, dx <= 1 && dy <= 1. Any grid size + // which is not in this list is not migrated. + // Note that the InvariantDeviceProfile defines (rows, cols) but the Points + // specified here are defined as (cols, rows). + ArrayList gridSizeSteps = new ArrayList<>(); + gridSizeSteps.add(new Point(3, 2)); + gridSizeSteps.add(new Point(3, 3)); + gridSizeSteps.add(new Point(4, 3)); + gridSizeSteps.add(new Point(4, 4)); + gridSizeSteps.add(new Point(5, 5)); + gridSizeSteps.add(new Point(6, 5)); + gridSizeSteps.add(new Point(6, 6)); + gridSizeSteps.add(new Point(7, 7)); + + int sourceSizeIndex = gridSizeSteps.indexOf(sourceSize); + int targetSizeIndex = gridSizeSteps.indexOf(targetSize); + + if (sourceSizeIndex <= -1 || targetSizeIndex <= -1) { + throw new Exception("Unable to migrate grid size from " + sourceSize + + " to " + targetSize); + } + + // Min widget sizes + HashMap widgetMinSize = new HashMap<>(); + for (String s : Utilities.getPrefs(context).getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, + Collections.emptySet())) { + String[] parts = s.split("#"); + widgetMinSize.put(parts[0], parsePoint(parts[1])); + } + + // Migrate the workspace grid, step by step. + while (targetSizeIndex < sourceSizeIndex ) { + // We only need to migrate the grid if source size is greater + // than the target size. + Point stepTargetSize = gridSizeSteps.get(sourceSizeIndex - 1); + Point stepSourceSize = gridSizeSteps.get(sourceSizeIndex); + + if (new GridSizeMigrationTask(context, + LauncherAppState.getInstance().getInvariantDeviceProfile(), + validPackages, widgetMinSize, + stepSourceSize, stepTargetSize).migrateWorkspace()) { + dbChanged = true; + } + sourceSizeIndex--; + } + } + + if (dbChanged) { + // Make sure we haven't removed everything. + final Cursor c = context.getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); + boolean hasData = c.moveToNext(); + c.close(); + if (!hasData) { + throw new Exception("Removed every thing during grid resize"); + } + } + + return true; + } catch (Exception e) { + Log.e(TAG, "Error during grid migration", e); + + return false; + } finally { + Log.v(TAG, "Workspace migration completed in " + + (System.currentTimeMillis() - migrationStartTime)); + + // Save current configuration, so that the migration does not run again. + prefs.edit() + .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString) + .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString) + .remove(KEY_MIGRATION_WIDGET_MINSIZE) + .apply(); + } } } diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java index eef4f9173a..53e5213139 100644 --- a/src/com/android/launcher3/model/WidgetsModel.java +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -1,9 +1,13 @@ package com.android.launcher3.model; +import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.ResolveInfo; +import android.os.DeadObjectException; +import android.os.TransactionTooLargeException; import android.util.Log; import com.android.launcher3.AppFilter; @@ -16,6 +20,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.compat.AlphabeticIndexCompat; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.config.ProviderConfig; import java.util.ArrayList; import java.util.Collections; @@ -34,19 +39,19 @@ public class WidgetsModel { private static final boolean DEBUG = false; /* List of packages that is tracked by this model. */ - private ArrayList mPackageItemInfos = new ArrayList<>(); + private final ArrayList mPackageItemInfos; /* Map of widgets and shortcuts that are tracked per package. */ - private HashMap> mWidgetsList = new HashMap<>(); - - private ArrayList mRawList; + private final HashMap> mWidgetsList; private final AppWidgetManagerCompat mAppWidgetMgr; private final WidgetsAndShortcutNameComparator mWidgetAndShortcutNameComparator; private final Comparator mAppNameComparator; private final IconCache mIconCache; private final AppFilter mAppFilter; - private AlphabeticIndexCompat mIndexer; + private final AlphabeticIndexCompat mIndexer; + + private ArrayList mRawList; public WidgetsModel(Context context, IconCache iconCache, AppFilter appFilter) { mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context); @@ -55,6 +60,10 @@ public class WidgetsModel { mIconCache = iconCache; mAppFilter = appFilter; mIndexer = new AlphabeticIndexCompat(context); + mPackageItemInfos = new ArrayList<>(); + mWidgetsList = new HashMap<>(); + + mRawList = new ArrayList<>(); } @SuppressWarnings("unchecked") @@ -62,18 +71,16 @@ public class WidgetsModel { mAppWidgetMgr = model.mAppWidgetMgr; mPackageItemInfos = (ArrayList) model.mPackageItemInfos.clone(); mWidgetsList = (HashMap>) model.mWidgetsList.clone(); - mRawList = (ArrayList) model.mRawList.clone(); mWidgetAndShortcutNameComparator = model.mWidgetAndShortcutNameComparator; mAppNameComparator = model.mAppNameComparator; mIconCache = model.mIconCache; mAppFilter = model.mAppFilter; + mIndexer = model.mIndexer; + mRawList = (ArrayList) model.mRawList.clone(); } // Access methods that may be deleted if the private fields are made package-private. public int getPackageSize() { - if (mPackageItemInfos == null) { - return 0; - } return mPackageItemInfos.size(); } @@ -93,8 +100,41 @@ public class WidgetsModel { return mRawList; } - public void setWidgetsAndShortcuts(ArrayList rawWidgetsShortcuts) { + public boolean isEmpty() { + return mRawList.isEmpty(); + } + + public WidgetsModel updateAndClone(Context context) { Utilities.assertWorkerThread(); + + try { + final ArrayList widgetsAndShortcuts = new ArrayList<>(); + // Widgets + for (AppWidgetProviderInfo widgetInfo : + AppWidgetManagerCompat.getInstance(context).getAllProviders()) { + widgetsAndShortcuts.add(LauncherAppWidgetProviderInfo + .fromProviderInfo(context, widgetInfo)); + } + // Shortcuts + widgetsAndShortcuts.addAll(context.getPackageManager().queryIntentActivities( + new Intent(Intent.ACTION_CREATE_SHORTCUT), 0)); + setWidgetsAndShortcuts(widgetsAndShortcuts); + } catch (Exception e) { + if (!LauncherAppState.isDogfoodBuild() && + (e.getCause() instanceof TransactionTooLargeException || + e.getCause() instanceof DeadObjectException)) { + // the returned value may be incomplete and will not be refreshed until the next + // time Launcher starts. + // TODO: after figuring out a repro step, introduce a dirty bit to check when + // onResume is called to refresh the widget provider list. + } else { + throw e; + } + } + return clone(); + } + + private void setWidgetsAndShortcuts(ArrayList rawWidgetsShortcuts) { mRawList = rawWidgetsShortcuts; if (DEBUG) { Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size()); diff --git a/src/com/android/launcher3/DummyWidget.java b/src/com/android/launcher3/testing/DummyWidget.java similarity index 82% rename from src/com/android/launcher3/DummyWidget.java rename to src/com/android/launcher3/testing/DummyWidget.java index 59cd805012..df887ac1fc 100644 --- a/src/com/android/launcher3/DummyWidget.java +++ b/src/com/android/launcher3/testing/DummyWidget.java @@ -1,7 +1,10 @@ -package com.android.launcher3; +package com.android.launcher3.testing; import android.appwidget.AppWidgetProviderInfo; +import com.android.launcher3.CustomAppWidget; +import com.android.launcher3.R; + public class DummyWidget implements CustomAppWidget { @Override public String getLabel() { @@ -20,7 +23,7 @@ public class DummyWidget implements CustomAppWidget { @Override public int getWidgetLayout() { - return R.layout.dummy_widget; + return R.layout.zzz_dummy_widget; } @Override diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java index 8702877bf5..1bb57874e3 100644 --- a/src/com/android/launcher3/testing/LauncherExtension.java +++ b/src/com/android/launcher3/testing/LauncherExtension.java @@ -5,11 +5,13 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.ComponentName; import android.content.Intent; +import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import com.android.launcher3.AppInfo; import com.android.launcher3.InsettableFrameLayout; @@ -210,13 +212,39 @@ public class LauncherExtension extends Launcher { public void startVoice() { } + CustomContentCallbacks mCustomContentCallbacks = new CustomContentCallbacks() { + + // Custom content is completely shown. {@code fromResume} indicates whether this was caused + // by a onResume or by scrolling otherwise. + public void onShow(boolean fromResume) { + } + + // Custom content is completely hidden + public void onHide() { + } + + // Custom content scroll progress changed. From 0 (not showing) to 1 (fully showing). + public void onScrollProgressChanged(float progress) { + + } + + // Indicates whether the user is allowed to scroll away from the custom content. + public boolean isScrollingAllowed() { + return true; + } + + }; + @Override public boolean hasCustomContentToLeft() { - return false; + return true; } @Override public void populateCustomContentContainer() { + FrameLayout customContent = new FrameLayout(LauncherExtension.this); + customContent.setBackgroundColor(Color.GRAY); + addToCustomContentPage(customContent, mCustomContentCallbacks, ""); } @Override @@ -224,6 +252,11 @@ public class LauncherExtension extends Launcher { return mLauncherOverlay.getSearchBox(); } + @Override + public Bundle getAdditionalSearchWidgetOptions() { + return new Bundle(); + } + @Override public Intent getFirstRunActivity() { return null; @@ -274,6 +307,11 @@ public class LauncherExtension extends Launcher { return new ArrayList<>(); } + @Override + public int getSearchBarHeight() { + return SEARCH_BAR_HEIGHT_NORMAL; + } + @Override public boolean isLauncherPreinstalled() { return false; @@ -281,7 +319,7 @@ public class LauncherExtension extends Launcher { @Override public boolean hasLauncherOverlay() { - return true; + return false; } @Override diff --git a/src/com/android/launcher3/MemoryDumpActivity.java b/src/com/android/launcher3/testing/MemoryDumpActivity.java similarity index 94% rename from src/com/android/launcher3/MemoryDumpActivity.java rename to src/com/android/launcher3/testing/MemoryDumpActivity.java index d79be80073..9bcf92b1b2 100644 --- a/src/com/android/launcher3/MemoryDumpActivity.java +++ b/src/com/android/launcher3/testing/MemoryDumpActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.testing; import android.app.Activity; import android.content.ComponentName; @@ -23,10 +23,20 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.*; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.IBinder; import android.util.Log; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.zip.ZipEntry; diff --git a/src/com/android/launcher3/MemoryTracker.java b/src/com/android/launcher3/testing/MemoryTracker.java similarity index 93% rename from src/com/android/launcher3/MemoryTracker.java rename to src/com/android/launcher3/testing/MemoryTracker.java index 067a50f970..ed2a3122c0 100644 --- a/src/com/android/launcher3/MemoryTracker.java +++ b/src/com/android/launcher3/testing/MemoryTracker.java @@ -14,22 +14,28 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.testing; import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.*; +import android.os.Binder; +import android.os.Debug; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; +import com.android.launcher3.util.TestingUtils; + import java.util.ArrayList; import java.util.List; public class MemoryTracker extends Service { public static final String TAG = MemoryTracker.class.getSimpleName(); - public static final String ACTION_START_TRACKING = "com.android.launcher3.action.START_TRACKING"; private static final long UPDATE_RATE = 5000; @@ -83,14 +89,6 @@ public class MemoryTracker extends Service { ActivityManager mAm; - public static void startTrackingMe(Context context, String name) { - context.startService(new Intent(context, MemoryTracker.class) - .setAction(MemoryTracker.ACTION_START_TRACKING) - .putExtra("pid", android.os.Process.myPid()) - .putExtra("name", name) - ); - } - public ProcessMemInfo getMemInfo(int pid) { return mData.get(pid); } @@ -190,7 +188,7 @@ public class MemoryTracker extends Service { Log.v(TAG, "Received start id " + startId + ": " + intent); if (intent != null) { - if (ACTION_START_TRACKING.equals(intent.getAction())) { + if (TestingUtils.ACTION_START_TRACKING.equals(intent.getAction())) { final int pid = intent.getIntExtra("pid", -1); final String name = intent.getStringExtra("name"); final long start = intent.getLongExtra("start", System.currentTimeMillis()); diff --git a/src/com/android/launcher3/testing/ToggleWeightWatcher.java b/src/com/android/launcher3/testing/ToggleWeightWatcher.java new file mode 100644 index 0000000000..e08ec3aca0 --- /dev/null +++ b/src/com/android/launcher3/testing/ToggleWeightWatcher.java @@ -0,0 +1,31 @@ +package com.android.launcher3.testing; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.TestingUtils; + +public class ToggleWeightWatcher extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences sp = Utilities.getPrefs(this); + boolean show = sp.getBoolean(TestingUtils.SHOW_WEIGHT_WATCHER, true); + + show = !show; + sp.edit().putBoolean(TestingUtils.SHOW_WEIGHT_WATCHER, show).apply(); + + Launcher launcher = (Launcher) LauncherAppState.getInstance().getModel().getCallback(); + if (launcher != null && launcher.mWeightWatcher != null) { + launcher.mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE); + } + finish(); + } +} diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/testing/WeightWatcher.java similarity index 98% rename from src/com/android/launcher3/WeightWatcher.java rename to src/com/android/launcher3/testing/WeightWatcher.java index 75684797fa..a26a2b6420 100644 --- a/src/com/android/launcher3/WeightWatcher.java +++ b/src/com/android/launcher3/testing/WeightWatcher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.testing; import android.content.ComponentName; import android.content.Context; @@ -116,10 +116,6 @@ public class WeightWatcher extends LinearLayout { } } - public WeightWatcher(Context context) { - this(context, null); - } - @Override public void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java new file mode 100644 index 0000000000..bdb16390b2 --- /dev/null +++ b/src/com/android/launcher3/util/ConfigMonitor.java @@ -0,0 +1,63 @@ +package com.android.launcher3.util; + +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.util.Log; + +import com.android.launcher3.Utilities; + +/** + * {@link BroadcastReceiver} which watches configuration changes and + * restarts the process in case changes which affect the device profile occur. + */ +public class ConfigMonitor extends BroadcastReceiver { + + private final Context mContext; + private final float mFontScale; + private final int mDensity; + + public ConfigMonitor(Context context) { + mContext = context; + + Configuration config = context.getResources().getConfiguration(); + mFontScale = config.fontScale; + mDensity = getDensity(config); + } + + @Override + public void onReceive(Context context, Intent intent) { + Configuration config = context.getResources().getConfiguration(); + if (mFontScale != config.fontScale || mDensity != getDensity(config)) { + Log.d("ConfigMonitor", "Configuration changed, restarting launcher"); + mContext.unregisterReceiver(this); + android.os.Process.killProcess(android.os.Process.myPid()); + } + } + + public void register() { + mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); + } + + private static int getDensity(Configuration config) { + return Utilities.ATLEAST_JB_MR1 ? config.densityDpi : 0; + } +} diff --git a/src/com/android/launcher3/util/FlagOp.java b/src/com/android/launcher3/util/FlagOp.java new file mode 100644 index 0000000000..5e26ed12fe --- /dev/null +++ b/src/com/android/launcher3/util/FlagOp.java @@ -0,0 +1,30 @@ +package com.android.launcher3.util; + +public abstract class FlagOp { + + public static FlagOp NO_OP = new FlagOp() {}; + + private FlagOp() {} + + public int apply(int flags) { + return flags; + } + + public static FlagOp addFlag(final int flag) { + return new FlagOp() { + @Override + public int apply(int flags) { + return flags | flag; + } + }; + } + + public static FlagOp removeFlag(final int flag) { + return new FlagOp() { + @Override + public int apply(int flags) { + return flags & ~flag; + } + }; + } +} diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java index 696eabe002..a5498f7b9b 100644 --- a/src/com/android/launcher3/util/FocusLogic.java +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -22,9 +22,6 @@ import android.view.View; import android.view.ViewGroup; import com.android.launcher3.CellLayout; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.ShortcutAndWidgetContainer; import java.util.Arrays; @@ -51,7 +48,7 @@ public class FocusLogic { private static final String TAG = "FocusLogic"; private static final boolean DEBUG = false; - // Item and page index related constant used by {@link #handleKeyEvent}. + /** Item and page index related constant used by {@link #handleKeyEvent}. */ public static final int NOOP = -1; public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; @@ -66,6 +63,8 @@ public class FocusLogic { public static final int NEXT_PAGE_LEFT_COLUMN = -9; public static final int NEXT_PAGE_RIGHT_COLUMN = -10; + public static final int ALL_APPS_COLUMN = -11; + // Matrix related constant. public static final int EMPTY = -1; public static final int PIVOT = 100; @@ -81,8 +80,11 @@ public class FocusLogic { keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL); } - public static int handleKeyEvent(int keyCode, int cntX, int cntY, - int [][] map, int iconIdx, int pageIndex, int pageCount, boolean isRtl) { + public static int handleKeyEvent(int keyCode, int [][] map, int iconIdx, int pageIndex, + int pageCount, boolean isRtl) { + + int cntX = map == null ? -1 : map.length; + int cntY = map == null ? -1 : map[0].length; if (DEBUG) { Log.v(TAG, String.format( @@ -93,16 +95,16 @@ public class FocusLogic { int newIndex = NOOP; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: - newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); - if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/, isRtl); + if (!isRtl && newIndex == NOOP && pageIndex > 0) { newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { newIndex = NEXT_PAGE_RIGHT_COLUMN; } break; case KeyEvent.KEYCODE_DPAD_RIGHT: - newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); - if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/, isRtl); + if (!isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { newIndex = NEXT_PAGE_LEFT_COLUMN; } else if (isRtl && newIndex == NOOP && pageIndex > 0) { newIndex = PREVIOUS_PAGE_LEFT_COLUMN; @@ -168,8 +170,12 @@ public class FocusLogic { // Iterate thru the children. for (int i = 0; i < parent.getChildCount(); i++ ) { - int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX; - int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY; + View cell = parent.getChildAt(i); + if (!cell.isFocusable()) { + continue; + } + int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; matrix[invert ? (m - cx - 1) : cx][cy] = i; } if (DEBUG) { @@ -184,51 +190,67 @@ public class FocusLogic { * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] */ // TODO: get rid of the dynamic matrix creation - public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout, - boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) { + public static int[][] createSparseMatrixWithHotseat(CellLayout iconLayout, + CellLayout hotseatLayout, boolean isHotseatHorizontal, int allappsiconRank) { ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + boolean moreIconsInHotseatThanWorkspace = isHotseatHorizontal ? + hotseatLayout.getCountX() > iconLayout.getCountX() : + hotseatLayout.getCountY() > iconLayout.getCountY(); + int m, n; - if (isHorizontal) { - m = iconLayout.getCountX(); + if (isHotseatHorizontal) { + m = hotseatLayout.getCountX(); n = iconLayout.getCountY() + hotseatLayout.getCountY(); } else { m = iconLayout.getCountX() + hotseatLayout.getCountX(); - n = iconLayout.getCountY(); + n = hotseatLayout.getCountY(); } int[][] matrix = createFullMatrix(m, n); - - // Iterate thru the children of the top parent. + if (moreIconsInHotseatThanWorkspace) { + if (isHotseatHorizontal) { + for (int j = 0; j < n; j++) { + matrix[allappsiconRank][j] = ALL_APPS_COLUMN; + } + } else { + for (int j = 0; j < m; j++) { + matrix[j][allappsiconRank] = ALL_APPS_COLUMN; + } + } + } + // Iterate thru the children of the workspace. for (int i = 0; i < iconParent.getChildCount(); i++) { - int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; - int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + View cell = iconParent.getChildAt(i); + if (!cell.isFocusable()) { + continue; + } + int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; + if (moreIconsInHotseatThanWorkspace) { + if (isHotseatHorizontal && cx >= allappsiconRank) { + // Add 1 to account for the All Apps button. + cx++; + } + if (!isHotseatHorizontal && cy >= allappsiconRank) { + // Add 1 to account for the All Apps button. + cy++; + } + } matrix[cx][cy] = i; } - // Iterate thru the children of the bottom parent - // The hotseat view group contains one more item than iconLayout column count. - // If {@param allappsiconRank} not negative, then the last icon in the hotseat - // is truncated. If it is negative, then all apps icon index is not inserted. - for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) { - int delta = 0; - if (isHorizontal) { + // Iterate thru the children of the hotseat. + for (int i = hotseatParent.getChildCount() - 1; i >= 0; i--) { + if (isHotseatHorizontal) { int cx = ((CellLayout.LayoutParams) hotseatParent.getChildAt(i).getLayoutParams()).cellX; - if ((includeAllappsicon && cx >= allappsiconRank) || - (!includeAllappsicon && cx > allappsiconRank)) { - delta = -1; - } - matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i; + matrix[cx][iconLayout.getCountY()] = iconParent.getChildCount() + i; } else { int cy = ((CellLayout.LayoutParams) hotseatParent.getChildAt(i).getLayoutParams()).cellY; - if ((includeAllappsicon && cy >= allappsiconRank) || - (!includeAllappsicon && cy > allappsiconRank)) { - delta = -1; - } - matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i; + matrix[iconLayout.getCountX()][cy] = iconParent.getChildCount() + i; } } if (DEBUG) { @@ -248,7 +270,8 @@ public class FocusLogic { * @param pivotY y coordinate of the focused item in the current page */ // TODO: get rid of the dynamic matrix creation - public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) { + public static int[][] createSparseMatrixWithPivotColumn(CellLayout iconLayout, + int pivotX, int pivotY) { ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); @@ -256,8 +279,12 @@ public class FocusLogic { // Iterate thru the children of the top parent. for (int i = 0; i < iconParent.getChildCount(); i++) { - int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; - int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + View cell = iconParent.getChildAt(i); + if (!cell.isFocusable()) { + continue; + } + int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY; if (pivotX < 0) { matrix[cx - pivotX][cy] = i; } else { @@ -291,7 +318,7 @@ public class FocusLogic { */ // TODO: add unit tests to verify all permutation. private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, - int[][] matrix, int increment) { + int[][] matrix, int increment, boolean isRtl) { if(matrix == null) { throw new IllegalStateException("Dpad navigation requires a matrix."); } @@ -314,8 +341,9 @@ public class FocusLogic { } // Rule1: check first in the horizontal direction - for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) { - if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) { + for (int x = xPos + increment; 0 <= x && x < cntX; x += increment) { + if ((newIconIndex = inspectMatrix(x, yPos, cntX, cntY, matrix)) != NOOP + && newIconIndex != ALL_APPS_COLUMN) { return newIconIndex; } } @@ -324,19 +352,41 @@ public class FocusLogic { // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) int nextYPos1; int nextYPos2; - int i = -1; + boolean haveCrossedAllAppsColumn1 = false; + boolean haveCrossedAllAppsColumn2 = false; + int x = -1; for (int coeff = 1; coeff < cntY; coeff++) { nextYPos1 = yPos + coeff * increment; nextYPos2 = yPos - coeff * increment; - for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) { - if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) { + x = xPos + increment * coeff; + if (inspectMatrix(x, nextYPos1, cntX, cntY, matrix) == ALL_APPS_COLUMN) { + haveCrossedAllAppsColumn1 = true; + } + if (inspectMatrix(x, nextYPos2, cntX, cntY, matrix) == ALL_APPS_COLUMN) { + haveCrossedAllAppsColumn2 = true; + } + for (; 0 <= x && x < cntX; x += increment) { + int offset1 = haveCrossedAllAppsColumn1 && x < cntX - 1 ? increment : 0; + newIconIndex = inspectMatrix(x, nextYPos1 + offset1, cntX, cntY, matrix); + if (newIconIndex != NOOP) { return newIconIndex; } - if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) { + int offset2 = haveCrossedAllAppsColumn2 && x < cntX - 1 ? -increment : 0; + newIconIndex = inspectMatrix(x, nextYPos2 + offset2, cntX, cntY, matrix); + if (newIconIndex != NOOP) { return newIconIndex; } } } + + // Rule3: if switching between pages, do a brute-force search to find an item that was + // missed by rules 1 and 2 (such as when going from a bottom right icon to top left) + if (iconIdx == PIVOT) { + if (isRtl) { + return increment < 0 ? NEXT_PAGE_FIRST_ITEM : PREVIOUS_PAGE_LAST_ITEM; + } + return increment < 0 ? PREVIOUS_PAGE_LAST_ITEM : NEXT_PAGE_FIRST_ITEM; + } return newIconIndex; } @@ -375,8 +425,9 @@ public class FocusLogic { } // Rule1: check first in the dpad direction - for (int j = yPos + increment; 0 <= j && j mMaxSize ? mMaxSize : width; + height = height <= 0 || height > mMaxSize ? mMaxSize : height; + } else if (width > mMaxSize || height > mMaxSize) { + int max = Math.max(width, height); + width = mMaxSize * width / max; + height = mMaxSize * height / max; + } + + mBitmap.eraseColor(Color.TRANSPARENT); + d.setBounds(0, 0, width, height); + d.draw(mCanvas); + + ByteBuffer buffer = ByteBuffer.wrap(mPixels); + buffer.rewind(); + mBitmap.copyPixelsToBuffer(buffer); + + // Overall bounds of the visible icon. + int topY = -1; + int bottomY = -1; + int leftX = mMaxSize + 1; + int rightX = -1; + + // Create border by going through all pixels one row at a time and for each row find + // the first and the last non-transparent pixel. Set those values to mLeftBorder and + // mRightBorder and use -1 if there are no visible pixel in the row. + + // buffer position + int index = 0; + // buffer shift after every row, width of buffer = mMaxSize + int rowSizeDiff = mMaxSize - width; + // first and last position for any row. + int firstX, lastX; + + for (int y = 0; y < height; y++) { + firstX = lastX = -1; + for (int x = 0; x < width; x++) { + if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) { + if (firstX == -1) { + firstX = x; + } + lastX = x; + } + index++; + } + index += rowSizeDiff; + + mLeftBorder[y] = firstX; + mRightBorder[y] = lastX; + + // If there is at least one visible pixel, update the overall bounds. + if (firstX != -1) { + bottomY = y; + if (topY == -1) { + topY = y; + } + + leftX = Math.min(leftX, firstX); + rightX = Math.max(rightX, lastX); + } + } + + if (topY == -1 || rightX == -1) { + // No valid pixels found. Do not scale. + return 1; + } + + convertToConvexArray(mLeftBorder, 1, topY, bottomY); + convertToConvexArray(mRightBorder, -1, topY, bottomY); + + // Area of the convex hull + float area = 0; + for (int y = 0; y < height; y++) { + if (mLeftBorder[y] <= -1) { + continue; + } + area += mRightBorder[y] - mLeftBorder[y] + 1; + } + + // Area of the rectangle required to fit the convex hull + float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX); + float hullByRect = area / rectArea; + + float scaleRequired; + if (hullByRect < CIRCLE_AREA_BY_RECT) { + scaleRequired = MAX_CIRCLE_AREA_FACTOR; + } else { + scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); + } + + float areaScale = area / (width * height); + // Use sqrt of the final ratio as the images is scaled across both width and height. + float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; + return scale; + } + + /** + * Modifies {@param xCordinates} to represent a convex border. Fills in all missing values + * (except on either ends) with appropriate values. + * @param xCordinates map of x coordinate per y. + * @param direction 1 for left border and -1 for right border. + * @param topY the first Y position (inclusive) with a valid value. + * @param bottomY the last Y position (inclusive) with a valid value. + */ + private static void convertToConvexArray( + float[] xCordinates, int direction, int topY, int bottomY) { + int total = xCordinates.length; + // The tangent at each pixel. + float[] angles = new float[total - 1]; + + int first = topY; // First valid y coordinate + int last = -1; // Last valid y coordinate which didn't have a missing value + + float lastAngle = Float.MAX_VALUE; + + for (int i = topY + 1; i <= bottomY; i++) { + if (xCordinates[i] <= -1) { + continue; + } + int start; + + if (lastAngle == Float.MAX_VALUE) { + start = first; + } else { + float currentAngle = (xCordinates[i] - xCordinates[last]) / (i - last); + start = last; + // If this position creates a concave angle, keep moving up until we find a + // position which creates a convex angle. + if ((currentAngle - lastAngle) * direction < 0) { + while (start > first) { + start --; + currentAngle = (xCordinates[i] - xCordinates[start]) / (i - start); + if ((currentAngle - angles[start]) * direction >= 0) { + break; + } + } + } + } + + // Reset from last check + lastAngle = (xCordinates[i] - xCordinates[start]) / (i - start); + // Update all the points from start. + for (int j = start; j < i; j++) { + angles[j] = lastAngle; + xCordinates[j] = xCordinates[start] + lastAngle * (j - start); + } + last = i; + } + } + + public static IconNormalizer getInstance() { + synchronized (LOCK) { + if (sIconNormalizer == null) { + sIconNormalizer = new IconNormalizer(); + } + } + return sIconNormalizer; + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java index 74fc92a04e..fb9bbb29c5 100644 --- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -42,6 +42,7 @@ import com.android.launcher3.compat.UserManagerCompat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -85,6 +86,7 @@ public class ManagedProfileHeuristic { private ArrayList mHomescreenApps; private ArrayList mWorkFolderApps; + private HashMap mShortcutToInstallTimeMap; private ManagedProfileHeuristic(Context context, UserHandleCompat user) { mContext = context; @@ -100,32 +102,29 @@ public class ManagedProfileHeuristic { Context.MODE_PRIVATE); } + private void initVars() { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + mShortcutToInstallTimeMap = new HashMap<>(); + } + /** * Checks the list of user apps and adds icons for newly installed apps on the homescreen or * workfolder. */ public void processUserApps(List apps) { - mHomescreenApps = new ArrayList<>(); - mWorkFolderApps = new ArrayList<>(); + initVars(); HashSet packageSet = new HashSet<>(); final boolean userAppsExisted = getUserApps(packageSet); boolean newPackageAdded = false; - for (LauncherActivityInfoCompat info : apps) { String packageName = info.getComponentName().getPackageName(); if (!packageSet.contains(packageName)) { packageSet.add(packageName); newPackageAdded = true; - - try { - PackageInfo pkgInfo = mContext.getPackageManager() - .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); - markForAddition(info, pkgInfo.firstInstallTime); - } catch (NameNotFoundException e) { - Log.e(TAG, "Unknown package " + packageName, e); - } + markForAddition(info, info.getFirstInstallTime()); } } @@ -142,7 +141,22 @@ public class ManagedProfileHeuristic { ArrayList targetList = (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ? mWorkFolderApps : mHomescreenApps; - targetList.add(ShortcutInfo.fromActivityInfo(info, mContext)); + ShortcutInfo si = ShortcutInfo.fromActivityInfo(info, mContext); + mShortcutToInstallTimeMap.put(si, installTime); + targetList.add(si); + } + + private void sortList(ArrayList infos) { + Collections.sort(infos, new Comparator() { + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + Long lhsTime = mShortcutToInstallTimeMap.get(lhs); + Long rhsTime = mShortcutToInstallTimeMap.get(rhs); + return Utilities.longCompare(lhsTime == null ? 0 : lhsTime, + rhsTime == null ? 0 : rhsTime); + } + }); } /** @@ -152,13 +166,7 @@ public class ManagedProfileHeuristic { if (mWorkFolderApps.isEmpty()) { return; } - Collections.sort(mWorkFolderApps, new Comparator() { - - @Override - public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { - return Long.compare(lhs.firstInstallTime, rhs.firstInstallTime); - } - }); + sortList(mWorkFolderApps); // Try to get a work folder. String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial; @@ -222,6 +230,7 @@ public class ManagedProfileHeuristic { finalizeWorkFolder(); if (addHomeScreenShortcuts && !mHomescreenApps.isEmpty()) { + sortList(mHomescreenApps); mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps); } } @@ -230,9 +239,7 @@ public class ManagedProfileHeuristic { * Updates the list of installed apps and adds any new icons on homescreen or work folder. */ public void processPackageAdd(String[] packages) { - mHomescreenApps = new ArrayList<>(); - mWorkFolderApps = new ArrayList<>(); - + initVars(); HashSet packageSet = new HashSet<>(); final boolean userAppsExisted = getUserApps(packageSet); diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java new file mode 100644 index 0000000000..d0345727fb --- /dev/null +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +/** + * Utility methods using package manager + */ +public class PackageManagerHelper { + + private static final int FLAG_SUSPENDED = 1<<30; + + /** + * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't + * guarantee that the app is on SD card. + */ + public static boolean isAppOnSdcard(PackageManager pm, String packageName) { + return isAppEnabled(pm, packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + } + + public static boolean isAppEnabled(PackageManager pm, String packageName) { + return isAppEnabled(pm, packageName, 0); + } + + public static boolean isAppEnabled(PackageManager pm, String packageName, int flags) { + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, flags); + return info != null && info.enabled; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static boolean isAppSuspended(PackageManager pm, String packageName) { + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + return info != null && isAppSuspended(info); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static boolean isAppSuspended(ApplicationInfo info) { + return (info.flags & FLAG_SUSPENDED) != 0; + } +} diff --git a/src/com/android/launcher3/util/ParcelableSparseArray.java b/src/com/android/launcher3/util/ParcelableSparseArray.java new file mode 100644 index 0000000000..093577e593 --- /dev/null +++ b/src/com/android/launcher3/util/ParcelableSparseArray.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.util; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +public class ParcelableSparseArray extends SparseArray implements Parcelable { + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + final int count = size(); + dest.writeInt(count); + for (int i = 0; i < count; i++) { + dest.writeInt(keyAt(i)); + dest.writeParcelable(valueAt(i), 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ParcelableSparseArray createFromParcel(Parcel source) { + final ParcelableSparseArray array = new ParcelableSparseArray(); + final ClassLoader loader = array.getClass().getClassLoader(); + final int count = source.readInt(); + for (int i = 0; i < count; i++) { + array.put(source.readInt(), source.readParcelable(loader)); + } + return array; + } + + public ParcelableSparseArray[] newArray(int size) { + return new ParcelableSparseArray[size]; + } + }; +} diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java new file mode 100644 index 0000000000..62a30d0d49 --- /dev/null +++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java @@ -0,0 +1,128 @@ +package com.android.launcher3.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteFullException; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +/** + * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB. + * Any exception during write operations are ignored, and any version change causes a DB reset. + */ +public abstract class SQLiteCacheHelper { + private static final String TAG = "SQLiteCacheHelper"; + + private final String mTableName; + private final MySQLiteOpenHelper mOpenHelper; + + private boolean mIgnoreWrites; + + public SQLiteCacheHelper(Context context, String name, int version, String tableName) { + mTableName = tableName; + mOpenHelper = new MySQLiteOpenHelper(context, name, version); + + mIgnoreWrites = false; + } + + /** + * @see SQLiteDatabase#update(String, ContentValues, String, String[]) + */ + public void update(ContentValues values, String whereClause, String[] whereArgs) { + if (mIgnoreWrites) { + return; + } + try { + mOpenHelper.getWritableDatabase().update(mTableName, values, whereClause, whereArgs); + } catch (SQLiteFullException e) { + onDiskFull(e); + } catch (SQLiteException e) { + Log.d(TAG, "Ignoring sqlite exception", e); + } + } + + /** + * @see SQLiteDatabase#delete(String, String, String[]) + */ + public void delete(String whereClause, String[] whereArgs) { + if (mIgnoreWrites) { + return; + } + try { + mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs); + } catch (SQLiteFullException e) { + onDiskFull(e); + } catch (SQLiteException e) { + Log.d(TAG, "Ignoring sqlite exception", e); + } + } + + /** + * @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int) + */ + public void insertOrReplace(ContentValues values) { + if (mIgnoreWrites) { + return; + } + try { + mOpenHelper.getWritableDatabase().insertWithOnConflict( + mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLiteFullException e) { + onDiskFull(e); + } catch (SQLiteException e) { + Log.d(TAG, "Ignoring sqlite exception", e); + } + } + + private void onDiskFull(SQLiteFullException e) { + Log.e(TAG, "Disk full, all write operations will be ignored", e); + mIgnoreWrites = true; + } + + /** + * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String) + */ + public Cursor query(String[] columns, String selection, String[] selectionArgs) { + return mOpenHelper.getReadableDatabase().query( + mTableName, columns, selection, selectionArgs, null, null, null); + } + + protected abstract void onCreateTable(SQLiteDatabase db); + + /** + * A private inner class to prevent direct DB access. + */ + private class MySQLiteOpenHelper extends SQLiteOpenHelper { + + public MySQLiteOpenHelper(Context context, String name, int version) { + super(context, name, null, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + onCreateTable(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + mTableName); + onCreate(db); + } + } +} diff --git a/src/com/android/launcher3/util/StringFilter.java b/src/com/android/launcher3/util/StringFilter.java new file mode 100644 index 0000000000..f539ad11e8 --- /dev/null +++ b/src/com/android/launcher3/util/StringFilter.java @@ -0,0 +1,31 @@ +package com.android.launcher3.util; + +import java.util.Set; + +/** + * Abstract class to filter a set of strings. + */ +public abstract class StringFilter { + + private StringFilter() { } + + public abstract boolean matches(String str); + + public static StringFilter matchesAll() { + return new StringFilter() { + @Override + public boolean matches(String str) { + return true; + } + }; + } + + public static StringFilter of(final Set validEntries) { + return new StringFilter() { + @Override + public boolean matches(String str) { + return validEntries.contains(str); + } + }; + } +} diff --git a/src/com/android/launcher3/util/TestingUtils.java b/src/com/android/launcher3/util/TestingUtils.java new file mode 100644 index 0000000000..665c371755 --- /dev/null +++ b/src/com/android/launcher3/util/TestingUtils.java @@ -0,0 +1,72 @@ +package com.android.launcher3.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.launcher3.CustomAppWidget; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; + +import java.util.HashMap; + +public class TestingUtils { + + public static final String MEMORY_TRACKER = "com.android.launcher3.testing.MemoryTracker"; + public static final String ACTION_START_TRACKING = "com.android.launcher3.action.START_TRACKING"; + + public static final boolean MEMORY_DUMP_ENABLED = false; + public static final String SHOW_WEIGHT_WATCHER = "debug.show_mem"; + + public static final boolean ENABLE_CUSTOM_WIDGET_TEST = false; + public static final String DUMMY_WIDGET = "com.android.launcher3.testing.DummyWidget"; + + public static void startTrackingMemory(Context context) { + if (MEMORY_DUMP_ENABLED) { + context.startService(new Intent() + .setComponent(new ComponentName(context.getPackageName(), MEMORY_TRACKER)) + .setAction(ACTION_START_TRACKING) + .putExtra("pid", android.os.Process.myPid()) + .putExtra("name", "L")); + } + } + + public static void addWeightWatcher(Launcher launcher) { + if (MEMORY_DUMP_ENABLED) { + boolean show = Utilities.getPrefs(launcher).getBoolean(SHOW_WEIGHT_WATCHER, true); + + int id = launcher.getResources().getIdentifier("zzz_weight_watcher", "layout", + launcher.getPackageName()); + View watcher = launcher.getLayoutInflater().inflate(id, null); + watcher.setAlpha(0.5f); + ((FrameLayout) launcher.findViewById(R.id.launcher)).addView(watcher, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM) + ); + + watcher.setVisibility(show ? View.VISIBLE : View.GONE); + launcher.mWeightWatcher = watcher; + } + } + + public static void addDummyWidget(HashMap set) { + if (ENABLE_CUSTOM_WIDGET_TEST) { + try { + Class clazz = Class.forName(DUMMY_WIDGET); + CustomAppWidget widget = (CustomAppWidget) clazz.newInstance(); + set.put(widget.getClass().getName(), widget); + } catch (Exception e) { + Log.e("TestingUtils", "Error adding dummy widget", e); + } + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java index 461aebb6b8..5d3af52542 100644 --- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -4,13 +4,13 @@ import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.content.Context; import android.graphics.Rect; -import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.view.View; import com.android.launcher3.AppWidgetResizeFrame; -import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragController; import com.android.launcher3.DragLayer; import com.android.launcher3.DragSource; import com.android.launcher3.Launcher; @@ -19,7 +19,9 @@ import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.util.Thunk; -public class WidgetHostViewLoader implements DragListener { +public class WidgetHostViewLoader implements DragController.DragListener { + private static final String TAG = "WidgetHostViewLoader"; + private static final boolean LOGD = false; /* Runnables to handle inflation and binding. */ @Thunk Runnable mInflateWidgetRunnable = null; @@ -48,6 +50,10 @@ public class WidgetHostViewLoader implements DragListener { @Override public void onDragEnd() { + if (LOGD) { + Log.d(TAG, "Cleaning up in onDragEnd()..."); + } + // Cleanup up preloading state. mLauncher.getDragController().removeDragListener(this); @@ -62,6 +68,9 @@ public class WidgetHostViewLoader implements DragListener { // The widget was inflated and added to the DragLayer -- remove it. if (mInfo.boundWidget != null) { + if (LOGD) { + Log.d(TAG, "...removing widget from drag layer"); + } mLauncher.getDragLayer().removeView(mInfo.boundWidget); mLauncher.getAppWidgetHost().deleteAppWidgetId(mInfo.boundWidget.getAppWidgetId()); mInfo.boundWidget = null; @@ -89,6 +98,9 @@ public class WidgetHostViewLoader implements DragListener { @Override public void run() { mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + if (LOGD) { + Log.d(TAG, "Binding widget, id: " + mWidgetLoadingId); + } if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( mWidgetLoadingId, pInfo, options)) { @@ -101,6 +113,9 @@ public class WidgetHostViewLoader implements DragListener { mInflateWidgetRunnable = new Runnable() { @Override public void run() { + if (LOGD) { + Log.d(TAG, "Inflating widget, id: " + mWidgetLoadingId); + } if (mWidgetLoadingId == -1) { return; } @@ -120,11 +135,17 @@ public class WidgetHostViewLoader implements DragListener { lp.x = lp.y = 0; lp.customPosition = true; hostView.setLayoutParams(lp); + if (LOGD) { + Log.d(TAG, "Adding host view to drag layer"); + } mLauncher.getDragLayer().addView(hostView); mView.setTag(mInfo); } }; + if (LOGD) { + Log.d(TAG, "About to bind/inflate widget"); + } mHandler.post(mBindWidgetRunnable); return true; } diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 0c6ea31bbb..c8e8cf8cd6 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -20,7 +20,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView.State; import android.util.AttributeSet; @@ -52,13 +51,9 @@ import com.android.launcher3.util.Thunk; * The widgets list view container. */ public class WidgetsContainerView extends BaseContainerView - implements View.OnLongClickListener, View.OnClickListener, DragSource{ - + implements View.OnLongClickListener, View.OnClickListener, DragSource { private static final String TAG = "WidgetsContainerView"; - private static final boolean DEBUG = false; - - /* Coefficient multiplied to the screen height for preloading widgets. */ - private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1; + private static final boolean LOGD = false; /* Global instances that are used inside this container. */ @Thunk Launcher mLauncher; @@ -66,8 +61,7 @@ public class WidgetsContainerView extends BaseContainerView private IconCache mIconCache; /* Recycler view related member variables */ - private View mContent; - private WidgetsRecyclerView mView; + private WidgetsRecyclerView mRecyclerView; private WidgetsListAdapter mAdapter; /* Touch handling related member variables. */ @@ -76,8 +70,6 @@ public class WidgetsContainerView extends BaseContainerView /* Rendering related. */ private WidgetPreviewLoader mWidgetPreviewLoader; - private Rect mPadding = new Rect(); - public WidgetsContainerView(Context context) { this(context, null); } @@ -92,45 +84,25 @@ public class WidgetsContainerView extends BaseContainerView mDragController = mLauncher.getDragController(); mAdapter = new WidgetsListAdapter(context, this, this, mLauncher); mIconCache = (LauncherAppState.getInstance()).getIconCache(); - if (DEBUG) { + if (LOGD) { Log.d(TAG, "WidgetsContainerView constructor"); } } @Override protected void onFinishInflate() { - mContent = findViewById(R.id.content); - mView = (WidgetsRecyclerView) findViewById(R.id.widgets_list_view); - mView.setAdapter(mAdapter); - - // This extends the layout space so that preloading happen for the {@link RecyclerView} - mView.setLayoutManager(new LinearLayoutManager(getContext()) { - @Override - protected int getExtraLayoutSpace(State state) { - DeviceProfile grid = mLauncher.getDeviceProfile(); - return super.getExtraLayoutSpace(state) - + grid.availableHeightPx * PRELOAD_SCREEN_HEIGHT_MULTIPLE; - } - }); - mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), - getPaddingBottom()); + super.onFinishInflate(); + mRecyclerView = (WidgetsRecyclerView) getContentView().findViewById(R.id.widgets_list_view); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); } // // Returns views used for launcher transitions. // - public View getContentView() { - return mView; - } - - public View getRevealView() { - // TODO(hyunyoungs): temporarily use apps view transition. - return findViewById(R.id.widgets_reveal_view); - } - public void scrollToTop() { - mView.scrollToPosition(0); + mRecyclerView.scrollToPosition(0); } // @@ -148,14 +120,17 @@ public class WidgetsContainerView extends BaseContainerView if (mWidgetInstructionToast != null) { mWidgetInstructionToast.cancel(); } - mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, - Toast.LENGTH_SHORT); + + CharSequence msg = Utilities.wrapForTts( + getContext().getText(R.string.long_press_widget_to_add), + getContext().getString(R.string.long_accessible_way_to_add)); + mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT); mWidgetInstructionToast.show(); } @Override public boolean onLongClick(View v) { - if (DEBUG) { + if (LOGD) { Log.d(TAG, String.format("onLonglick [v=%s]", v)); } // Return early if this is not initiated from a touch @@ -164,14 +139,13 @@ public class WidgetsContainerView extends BaseContainerView if (!mLauncher.isWidgetsViewVisible() || mLauncher.getWorkspace().isSwitchingState()) return false; // Return if global dragging is not enabled - Log.d(TAG, String.format("onLonglick dragging enabled?.", v)); if (!mLauncher.isDraggingEnabled()) return false; boolean status = beginDragging(v); if (status && v.getTag() instanceof PendingAddWidgetInfo) { WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v); boolean preloadStatus = hostLoader.preloadWidget(); - if (DEBUG) { + if (LOGD) { Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus)); } mLauncher.getDragController().addDragListener(hostLoader); @@ -300,6 +274,9 @@ public class WidgetsContainerView extends BaseContainerView @Override public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success) { + if (LOGD) { + Log.d(TAG, "onDropCompleted"); + } if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { // Exit spring loaded mode if we have not successfully dropped or have not handled the @@ -333,33 +310,32 @@ public class WidgetsContainerView extends BaseContainerView // // Container rendering related. // - @Override - protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { - // Apply the top-bottom padding to the content itself so that the launcher transition is - // clipped correctly - mContent.setPadding(0, padding.top, 0, padding.bottom); - - // TODO: Use quantum_panel_dark instead of quantum_panel_shape_dark. - InsetDrawable background = new InsetDrawable( - getResources().getDrawable(R.drawable.quantum_panel_shape_dark), padding.left, 0, - padding.right, 0); - Rect bgPadding = new Rect(); - background.getPadding(bgPadding); - mView.setBackground(background); - getRevealView().setBackground(background.getConstantState().newDrawable()); - mView.updateBackgroundPadding(bgPadding); + protected void onUpdateBgPadding(Rect padding, Rect bgPadding) { + if (Utilities.isRtl(getResources())) { + getContentView().setPadding(0, bgPadding.top, + bgPadding.right, bgPadding.bottom); + mRecyclerView.updateBackgroundPadding(new Rect(bgPadding.left, 0, 0, 0)); + } else { + getContentView().setPadding(bgPadding.left, bgPadding.top, + 0, bgPadding.bottom); + mRecyclerView.updateBackgroundPadding(new Rect(0, 0, bgPadding.right, 0)); + } } /** * Initialize the widget data model. */ public void addWidgets(WidgetsModel model) { - mView.setWidgets(model); + mRecyclerView.setWidgets(model); mAdapter.setWidgetsModel(model); mAdapter.notifyDataSetChanged(); } + public boolean isEmpty() { + return mAdapter.getItemCount() == 0; + } + private WidgetPreviewLoader getWidgetPreviewLoader() { if (mWidgetPreviewLoader == null) { mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java index f1cde299d0..885d96fe5f 100644 --- a/src/com/android/launcher3/widget/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -27,7 +27,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.view.ViewGroup.MarginLayoutParams; import android.widget.LinearLayout; import com.android.launcher3.BubbleTextView; diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java index 884bdc4181..fe9c51c447 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -102,9 +102,9 @@ public class WidgetsRecyclerView extends BaseRecyclerView { // Stop the scroller if it is scrolling stopScroll(); - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); float pos = rowCount * touchFraction; - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); + int availableScrollHeight = getAvailableScrollHeight(rowCount); LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); @@ -131,7 +131,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView { } // Skip early if, there no child laid out in the container. - getCurScrollState(mScrollPosState); + getCurScrollState(mScrollPosState, -1); if (mScrollPosState.rowIndex < 0) { mScrollbar.setThumbOffset(-1, -1); return; @@ -143,10 +143,10 @@ public class WidgetsRecyclerView extends BaseRecyclerView { /** * Returns the current scroll state. */ - protected void getCurScrollState(ScrollPositionState stateOut) { + protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; - stateOut.rowHeight = -1; + stateOut.itemPos = -1; // Skip early if widgets are not bound. if (mWidgets == null) { @@ -163,6 +163,17 @@ public class WidgetsRecyclerView extends BaseRecyclerView { stateOut.rowIndex = position; stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); - stateOut.rowHeight = child.getHeight(); + stateOut.itemPos = position; + } + + @Override + protected int getTop(int rowIndex) { + if (getChildCount() == 0) { + return 0; + } + + // All the rows are the same height, return any child height + View child = getChildAt(0); + return child.getMeasuredHeight() * rowIndex; } } \ No newline at end of file diff --git a/tests/Android.mk b/tests/Android.mk index eba4ade486..d82f0b35c6 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -14,21 +14,17 @@ # LOCAL_PATH := $(call my-dir) - -src_dirs := src -res_dirs := res - include $(CLEAR_VARS) +src_dirs := src LOCAL_MODULE_TAGS := tests - -LOCAL_STATIC_JAVA_LIBRARIES := android-support-test +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) LOCAL_AAPT_FLAGS := --auto-add-overlay -LOCAL_SDK_VERSION := 21 +LOCAL_SDK_VERSION := 23 LOCAL_PACKAGE_NAME := Launcher3Tests diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 42ae5a38a7..8acc5e90cd 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -1,4 +1,4 @@ - + + + + android:targetPackage="com.android.launcher3" > diff --git a/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java index 1bc7c1190f..21df60109f 100644 --- a/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java +++ b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java @@ -15,15 +15,14 @@ */ package com.android.launcher3; +import android.content.res.Resources; +import android.graphics.Point; import android.graphics.PointF; +import android.graphics.Rect; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.util.FocusLogic; - import java.util.ArrayList; /** @@ -52,9 +51,12 @@ public class InvariantDeviceProfileTest extends AndroidTestCase { public void testFindClosestDeviceProfile2() { for (InvariantDeviceProfile idf: mPredefinedDeviceProfiles) { + ArrayList predefinedProfilesCopy = + new ArrayList<>(mPredefinedDeviceProfiles); ArrayList closestProfiles = mInvariantProfile.findClosestDeviceProfiles( - idf.minWidthDps, idf.minHeightDps, mPredefinedDeviceProfiles); + idf.minWidthDps, idf.minHeightDps, predefinedProfilesCopy + ); assertTrue(closestProfiles.get(0).equals(idf)); } } @@ -106,18 +108,81 @@ public class InvariantDeviceProfileTest extends AndroidTestCase { if (!android.os.Build.DEVICE.equals("hammerhead")) { return; } - assertEquals(mInvariantProfile.numRows, 4); - assertEquals(mInvariantProfile.numColumns, 4); - assertEquals((int) mInvariantProfile.numHotseatIcons, 5); - - DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; - DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; - - assertEquals(portraitProfile.allAppsNumCols, 3); - assertEquals(landscapeProfile.allAppsNumCols, 5); // not used + assertEquals(4, mInvariantProfile.numRows); + assertEquals(4, mInvariantProfile.numColumns); + assertEquals(5, mInvariantProfile.numHotseatIcons); } // Add more tests for other devices, however, running them once on a single device is enough // for verifying that for a platform version, the WindowManager and DisplayMetrics is // working as intended. + + /** + * Make sure that the height for the QSB is what we expect in normal mode. + */ + public void testQsbNormalHeight() { + Resources resources = getContext().getResources(); + DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; + DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; + landscapeProfile.setSearchBarHeight(LauncherCallbacks.SEARCH_BAR_HEIGHT_NORMAL); + portraitProfile.setSearchBarHeight(LauncherCallbacks.SEARCH_BAR_HEIGHT_NORMAL); + Rect portraitBounds = portraitProfile.getSearchBarBounds(true); // RTL shouldn't matter. + int portraitHeight = (int) Utilities.dpiFromPx(portraitBounds.height(), + resources.getDisplayMetrics()); + Rect landscapeBounds = landscapeProfile.getSearchBarBounds(true); // RTL shouldn't matter. + int landscapeHeight = (int) Utilities.dpiFromPx(landscapeBounds.height(), + resources.getDisplayMetrics()); + if (portraitProfile.isTablet) { + assertEquals(8 + 48 + 24, portraitHeight); + } else { + assertEquals(8 + 48 + 12, portraitHeight); + } + // Make sure the height that we pass in the widget options bundle is the height of the + // search bar + 8dps padding top and bottom. + Point portraitDimens = portraitProfile.getSearchBarDimensForWidgetOpts(resources); + int portraitWidgetOptsHeight = portraitDimens.y; + Point landscapeDimens = landscapeProfile.getSearchBarDimensForWidgetOpts(resources); + int landscapeWidgetOptsHeight = landscapeDimens.y; + assertEquals(8 + 48 + 8, (int) Utilities.dpiFromPx(portraitWidgetOptsHeight, + resources.getDisplayMetrics())); + if (!landscapeProfile.isVerticalBarLayout()) { + assertEquals(portraitHeight, landscapeHeight); + assertEquals(portraitWidgetOptsHeight, landscapeWidgetOptsHeight); + } + } + + /** + * Make sure that the height for the QSB is what we expect in tall mode. + */ + public void testQsbTallHeight() { + Resources resources = getContext().getResources(); + DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; + DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; + landscapeProfile.setSearchBarHeight(LauncherCallbacks.SEARCH_BAR_HEIGHT_TALL); + portraitProfile.setSearchBarHeight(LauncherCallbacks.SEARCH_BAR_HEIGHT_TALL); + Rect portraitBounds = portraitProfile.getSearchBarBounds(true); // RTL shouldn't matter. + int portraitHeight = (int) Utilities.dpiFromPx(portraitBounds.height(), + resources.getDisplayMetrics()); + Rect landscapeBounds = landscapeProfile.getSearchBarBounds(true); // RTL shouldn't matter. + int landscapeHeight = (int) Utilities.dpiFromPx(landscapeBounds.height(), + resources.getDisplayMetrics()); + if (portraitProfile.isPhone) { + // This fails on some devices due to http://b/26884580 (portraitHeight is 101, not 100). + assertEquals(4 + 94 + 2, portraitHeight); + } else { + assertEquals(8 + 94 + 24, portraitHeight); + } + // Make sure the height that we pass in the widget options bundle is the height of the + // search bar + 8dps padding top and bottom. + Point portraitDimens = portraitProfile.getSearchBarDimensForWidgetOpts(resources); + int portraitWidgetOptsHeight = portraitDimens.y; + Point landscapeDimens = landscapeProfile.getSearchBarDimensForWidgetOpts(resources); + int landscapeWidgetOptsHeight = landscapeDimens.y; + assertEquals(8 + 94 + 8, (int) Utilities.dpiFromPx(portraitWidgetOptsHeight, + resources.getDisplayMetrics())); + if (!landscapeProfile.isVerticalBarLayout()) { + assertEquals(portraitHeight, landscapeHeight); + assertEquals(portraitWidgetOptsHeight, landscapeWidgetOptsHeight); + } + } } diff --git a/tests/src/com/android/launcher3/QuickAddWidgetTest.java b/tests/src/com/android/launcher3/QuickAddWidgetTest.java new file mode 100644 index 0000000000..027c465971 --- /dev/null +++ b/tests/src/com/android/launcher3/QuickAddWidgetTest.java @@ -0,0 +1,86 @@ +package com.android.launcher3; + +import android.content.Intent; +import android.graphics.Point; +import android.os.SystemClock; +import android.support.test.uiautomator.By; +import android.support.test.uiautomator.Direction; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject2; +import android.support.test.uiautomator.Until; +import android.test.InstrumentationTestCase; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import java.util.List; + +/** + * Add an arbitrary widget from the widget picker very quickly to test potential race conditions. + */ +public class QuickAddWidgetTest extends InstrumentationTestCase { + // Disabled because it's flaky and not particularly useful. But this class could still be useful + // as an example if we want other UI tests in the future. + private static final boolean DISABLED = true; + + private UiDevice mDevice; + private String mTargetPackage; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mDevice = UiDevice.getInstance(getInstrumentation()); + + // Set Launcher3 as home. + mTargetPackage = getInstrumentation().getTargetContext().getPackageName(); + Intent homeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(mTargetPackage) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getInstrumentation().getContext().startActivity(homeIntent); + mDevice.wait(Until.hasObject(By.pkg(mTargetPackage).depth(0)), 3000); + } + + public void testAddWidgetQuickly() throws Exception { + if (DISABLED) return; + mDevice.pressMenu(); // Enter overview mode. + mDevice.wait(Until.findObject(By.text("Widgets")), 3000).click(); + UiObject2 calendarWidget = getWidgetByName("Clock"); + Point center = calendarWidget.getVisibleCenter(); + // Touch widget just long enough to pick it up (longPressTimeout), then let go immediately. + getInstrumentation().sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, center.x, center.y, 0)); + Thread.sleep(ViewConfiguration.getLongPressTimeout() + 50); + getInstrumentation().sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, center.x, center.y, 0)); + + assertTrue("Drag was never started", isOnHomescreen()); + } + + private UiObject2 getWidgetByName(String name) { + UiObject2 widgetsList = mDevice.wait(Until.findObject(By.res(mTargetPackage, + "widgets_list_view")), 3000); + do { + UiObject2 widget = getVisibleWidgetByName(name); + if (widget != null) { + return widget; + } + } while (widgetsList.scroll(Direction.DOWN, 1f)); + return getVisibleWidgetByName(name); + } + + private UiObject2 getVisibleWidgetByName(String name) { + List visibleWidgets = mDevice.wait(Until.findObjects(By.clazz( + "android.widget.LinearLayout")), 3000); + for (UiObject2 widget : visibleWidgets) { + if (widget.hasObject(By.text(name))) { + return widget; + } + } + return null; + } + + private boolean isOnHomescreen() { + return mDevice.wait(Until.hasObject(By.res(mTargetPackage, "hotseat")), 3000); + } +} diff --git a/tests/src/com/android/launcher3/RotationPreferenceTest.java b/tests/src/com/android/launcher3/RotationPreferenceTest.java new file mode 100644 index 0000000000..0168ee66fd --- /dev/null +++ b/tests/src/com/android/launcher3/RotationPreferenceTest.java @@ -0,0 +1,88 @@ +package com.android.launcher3; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Rect; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiSelector; +import android.test.InstrumentationTestCase; + +/** + * Test for auto rotate preference. + */ +public class RotationPreferenceTest extends InstrumentationTestCase { + + private UiDevice mDevice; + private Context mTargetContext; + private String mTargetPackage; + + private SharedPreferences mPrefs; + private boolean mOriginalRotationValue; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mDevice = UiDevice.getInstance(getInstrumentation()); + mTargetContext = getInstrumentation().getTargetContext(); + mTargetPackage = mTargetContext.getPackageName(); + mPrefs = Utilities.getPrefs(mTargetContext); + mOriginalRotationValue = mPrefs.getBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, false); + } + + @Override + protected void tearDown() throws Exception { + setRotationEnabled(mOriginalRotationValue); + super.tearDown(); + } + + public void testRotation_disabled() throws Exception { + if (mTargetContext.getResources().getBoolean(R.bool.allow_rotation)) { + // This is a tablet. The test is only valid to mobile devices. + return; + } + + setRotationEnabled(false); + mDevice.setOrientationRight(); + goToLauncher(); + + Rect hotseat = getHotseatBounds(); + assertTrue(hotseat.width() > hotseat.height()); + } + + public void testRotation_enabled() throws Exception { + if (mTargetContext.getResources().getBoolean(R.bool.allow_rotation)) { + // This is a tablet. The test is only valid to mobile devices. + return; + } + + setRotationEnabled(true); + mDevice.setOrientationRight(); + goToLauncher(); + + Rect hotseat = getHotseatBounds(); + assertTrue(hotseat.width() < hotseat.height()); + } + + private void goToLauncher() { + Intent homeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(mTargetPackage) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getInstrumentation().getContext().startActivity(homeIntent); + mDevice.findObject(new UiSelector().packageName(mTargetPackage)).waitForExists(6000); + } + + private void setRotationEnabled(boolean enabled) { + mPrefs.edit().putBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, enabled).commit(); + } + + private Rect getHotseatBounds() throws Exception { + UiObject hotseat = mDevice.findObject( + new UiSelector().resourceId(mTargetPackage + ":id/hotseat")); + hotseat.waitForExists(6000); + return hotseat.getVisibleBounds(); + } +} diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java new file mode 100644 index 0000000000..ec157bc123 --- /dev/null +++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java @@ -0,0 +1,317 @@ +package com.android.launcher3.model; + +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Point; +import android.test.ProviderTestCase2; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.util.TestLauncherProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +/** + * Unit tests for {@link GridSizeMigrationTask} + */ +public class GridSizeMigrationTaskTest extends ProviderTestCase2 { + + private static final long DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; + private static final long HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; + + private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + + private static final String TEST_PACKAGE = "com.android.launcher3.validpackage"; + private static final String VALID_INTENT = + new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0); + + private HashSet mValidPackages; + private InvariantDeviceProfile mIdp; + + public GridSizeMigrationTaskTest() { + super(TestLauncherProvider.class, ProviderConfig.AUTHORITY); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mValidPackages = new HashSet<>(); + mValidPackages.add(TEST_PACKAGE); + + mIdp = new InvariantDeviceProfile(); + } + + public void testHotseatMigration_apps_dropped() throws Exception { + long[] hotseatItems = { + addItem(APPLICATION, 0, HOTSEAT, 0, 0), + addItem(SHORTCUT, 1, HOTSEAT, 0, 0), + -1, + addItem(SHORTCUT, 3, HOTSEAT, 0, 0), + addItem(APPLICATION, 4, HOTSEAT, 0, 0), + }; + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 2, 3, 1) + .migrateHotseat(); + // First & last items are dropped as they have the least weight. + verifyHotseat(hotseatItems[1], -1, hotseatItems[3]); + } + + public void testHotseatMigration_shortcuts_dropped() throws Exception { + long[] hotseatItems = { + addItem(APPLICATION, 0, HOTSEAT, 0, 0), + addItem(30, 1, HOTSEAT, 0, 0), + -1, + addItem(SHORTCUT, 3, HOTSEAT, 0, 0), + addItem(10, 4, HOTSEAT, 0, 0), + }; + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 2, 3, 1) + .migrateHotseat(); + // First & third items are dropped as they have the least weight. + verifyHotseat(hotseatItems[1], -1, hotseatItems[4]); + } + + private void verifyHotseat(long... sortedIds) { + int screenId = 0; + int total = 0; + + for (long id : sortedIds) { + Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-101 and screen=" + screenId, null, null, null); + + if (id == -1) { + assertEquals(0, c.getCount()); + } else { + assertEquals(1, c.getCount()); + c.moveToNext(); + assertEquals(id, c.getLong(0)); + total ++; + } + c.close(); + + screenId++; + } + + // Verify that not other entry exist in the DB. + Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-101", null, null, null); + assertEquals(total, c.getCount()); + c.close(); + } + + public void testWorkspace_empty_row_column_removed() throws Exception { + long[][][] ids = createGrid(new int[][][]{{ + { 0, 0, -1, 1}, + { 3, 1, -1, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap(), + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Column 2 and row 2 got removed. + verifyWorkspace(new long[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }}); + } + + public void testWorkspace_new_screen_created() throws Exception { + long[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap(), + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column get moved to new screen + verifyWorkspace(new long[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + public void testWorkspace_items_merged_in_next_screen() throws Exception { + long[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + },{ + { 0, 0, -1, 1}, + { 3, 1, -1, 4}, + }}); + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap(), + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on the 3rd + // row of the second screen + verifyWorkspace(new long[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + {ids[1][0][0], ids[1][0][1], ids[1][0][3]}, + {ids[1][1][0], ids[1][1][1], ids[1][1][3]}, + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + public void testWorkspace_items_not_merged_in_next_screen() throws Exception { + // First screen has 2 items that need to be moved, but second screen has only one + // empty space after migration (top-left corner) + long[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + },{ + { -1, 0, -1, 1}, + { 3, 1, -1, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap(), + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on a new screen. + verifyWorkspace(new long[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + { -1, ids[1][0][1], ids[1][0][3]}, + {ids[1][1][0], ids[1][1][1], ids[1][1][3]}, + {ids[1][3][0], ids[1][3][1], ids[1][3][3]}, + }, { + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + /** + * Initializes the DB with dummy elements to represent the provided grid structure. + * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for + * type definitions. The first dimension represents the screens and the next + * two represent the workspace grid. + * @return the same grid representation where each entry is the corresponding item id. + */ + private long[][][] createGrid(int[][][] typeArray) throws Exception { + long[][][] ids = new long[typeArray.length][][]; + + for (int i = 0; i < typeArray.length; i++) { + // Add screen to DB + long screenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); + ContentValues v = new ContentValues(); + v.put(LauncherSettings.WorkspaceScreens._ID, screenId); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + getMockContentResolver().insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v); + + ids[i] = new long[typeArray[i].length][]; + for (int y = 0; y < typeArray[i].length; y++) { + ids[i][y] = new long[typeArray[i][y].length]; + for (int x = 0; x < typeArray[i][y].length; x++) { + if (typeArray[i][y][x] < 0) { + // Empty cell + ids[i][y][x] = -1; + } else { + ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y); + } + } + } + } + return ids; + } + + /** + * Verifies that the workspace items are arranged in the provided order. + * @param ids A 3d array where the first dimension represents the screen, and the rest two + * represent the workspace grid. + */ + private void verifyWorkspace(long[][][] ids) { + ArrayList allScreens = LauncherModel.loadWorkspaceScreensDb(getMockContext()); + assertEquals(ids.length, allScreens.size()); + int total = 0; + + for (int i = 0; i < ids.length; i++) { + long screenId = allScreens.get(i); + for (int y = 0; y < ids[i].length; y++) { + for (int x = 0; x < ids[i][y].length; x++) { + long id = ids[i][y][x]; + + Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-100 and screen=" + screenId + + " and cellX=" + x + " and cellY=" + y, null, null, null); + if (id == -1) { + assertEquals(0, c.getCount()); + } else { + assertEquals(1, c.getCount()); + c.moveToNext(); + assertEquals(id, c.getLong(0)); + total++; + } + c.close(); + } + } + } + + // Verify that not other entry exist in the DB. + Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-100", null, null, null); + assertEquals(total, c.getCount()); + c.close(); + } + + /** + * Adds a dummy item in the DB. + * @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for + * folder (where the type represents the number of items in the folder). + */ + private long addItem(int type, long screen, long container, int x, int y) throws Exception { + long id = LauncherAppState.getLauncherProvider().generateNewItemId(); + + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites._ID, id); + values.put(LauncherSettings.Favorites.CONTAINER, container); + values.put(LauncherSettings.Favorites.SCREEN, screen); + values.put(LauncherSettings.Favorites.CELLX, x); + values.put(LauncherSettings.Favorites.CELLY, y); + values.put(LauncherSettings.Favorites.SPANX, 1); + values.put(LauncherSettings.Favorites.SPANY, 1); + + if (type == APPLICATION || type == SHORTCUT) { + values.put(LauncherSettings.Favorites.ITEM_TYPE, type); + values.put(LauncherSettings.Favorites.INTENT, VALID_INTENT); + } else { + values.put(LauncherSettings.Favorites.ITEM_TYPE, + LauncherSettings.Favorites.ITEM_TYPE_FOLDER); + // Add folder items. + for (int i = 0; i < type; i++) { + addItem(APPLICATION, 0, id, 0, 0); + } + } + + getMockContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values); + return id; + } +} diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java index ea87014e96..386f7dd345 100644 --- a/tests/src/com/android/launcher3/util/FocusLogicTest.java +++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java @@ -19,6 +19,7 @@ package com.android.launcher3; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.view.KeyEvent; +import android.view.View; import com.android.launcher3.util.FocusLogic; @@ -57,4 +58,200 @@ public final class FocusLogicTest extends AndroidTestCase { // may get created in real world to test this method. OR 2) Move all the matrix // management routine to celllayout and write tests for them. } + + public void testMoveFromBottomRightToBottomLeft() { + int[][] map = transpose(new int[][] { + {-1, 0, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {100, 1, -1, -1, -1, -1}, + }); + int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 100, 1, 2, false); + assertEquals(1, i); + } + + public void testMoveFromBottomRightToTopLeft() { + int[][] map = transpose(new int[][] { + {-1, 0, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1}, + {100, -1, -1, -1, -1, -1}, + }); + int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 100, 1, 2, false); + assertEquals(FocusLogic.NEXT_PAGE_FIRST_ITEM, i); + } + + public void testMoveIntoHotseatWithEqualHotseatAndWorkspaceColumns() { + // Test going from an icon right above the All Apps button to the All Apps button. + int[][] map = transpose(new int[][] { + {-1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1}, + {-1, -1, 0, -1, -1}, + { 2, 3, 1, 4, 5}, + }); + int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(1, i); + // Test going from an icon above and to the right of the All Apps + // button to an icon to the right of the All Apps button. + map = transpose(new int[][] { + {-1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1}, + {-1, -1, -1, 0, -1}, + { 2, 3, 1, 4, 5}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(4, i); + } + + public void testMoveIntoHotseatWithExtraColumnForAllApps() { + // Test going from an icon above and to the left + // of the All Apps button to the All Apps button. + int[][] map = transpose(new int[][] { + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, 0,-11, -1, -1, -1}, + {-1, -1, -1, 1, 1, -1, -1}, + }); + int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(1, i); + // Test going from an icon above and to the right + // of the All Apps button to the All Apps button. + map = transpose(new int[][] { + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, 0, -1, -1}, + {-1, -1, -1, 1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(1, i); + // Test going from the All Apps button to an icon + // above and to the right of the All Apps button. + map = transpose(new int[][] { + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, 0, -1, -1}, + {-1, -1, -1, 1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_UP, map, 1, 1, 1, true); + assertEquals(0, i); + // Test going from an icon above and to the left of the + // All Apps button in landscape to the All Apps button. + map = transpose(new int[][] { + { -1, -1, -1, -1, -1}, + { -1, -1, -1, 0, -1}, + {-11,-11,-11,-11, 1}, + { -1, -1, -1, -1, -1}, + { -1, -1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 0, 1, 1, true); + assertEquals(1, i); + // Test going from the All Apps button in landscape to + // an icon above and to the left of the All Apps button. + map = transpose(new int[][] { + { -1, -1, -1, -1, -1}, + { -1, -1, -1, 0, -1}, + {-11,-11,-11,-11, 1}, + { -1, -1, -1, -1, -1}, + { -1, -1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, map, 1, 1, 1, true); + assertEquals(0, i); + // Test that going to the hotseat always goes to the same row as the original icon. + map = transpose(new int[][]{ + { 0, 1, 2,-11, 3, 4, 5}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + {-1, -1, -1,-11, -1, -1, -1}, + { 7, 8, 9, 6, 10, 11, 12}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(7, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 1, 1, 1, true); + assertEquals(8, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 2, 1, 1, true); + assertEquals(9, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 3, 1, 1, true); + assertEquals(10, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 4, 1, 1, true); + assertEquals(11, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 5, 1, 1, true); + assertEquals(12, i); + } + + public void testCrossingAllAppsColumn() { + // Test crossing from left to right in portrait. + int[][] map = transpose(new int[][] { + {-1, -1,-11, -1, -1}, + {-1, 0,-11, -1, -1}, + {-1, -1,-11, 1, -1}, + {-1, -1,-11, -1, -1}, + {-1, -1, 2, -1, -1}, + }); + int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(1, i); + // Test crossing from right to left in portrait. + map = transpose(new int[][] { + {-1, -1,-11, -1, -1}, + {-1, -1,-11, 0, -1}, + {-1, 1,-11, -1, -1}, + {-1, -1,-11, -1, -1}, + {-1, -1, 2, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, map, 0, 1, 1, true); + assertEquals(1, i); + // Test crossing from left to right in landscape. + map = transpose(new int[][] { + { -1, -1, -1, -1, -1}, + { -1, -1, -1, 0, -1}, + {-11,-11,-11,-11, 2}, + { -1, 1, -1, -1, -1}, + { -1, -1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, map, 0, 1, 1, true); + assertEquals(1, i); + // Test crossing from right to left in landscape. + map = transpose(new int[][] { + { -1, -1, -1, -1, -1}, + { -1, 0, -1, -1, -1}, + {-11,-11,-11,-11, 2}, + { -1, -1, 1, -1, -1}, + { -1, -1, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 0, 1, 1, true); + assertEquals(1, i); + // Test NOT crossing it, if the All Apps button is the only suitable candidate. + map = transpose(new int[][]{ + {-1, 0, -1, -1, -1}, + {-1, 1, -1, -1, -1}, + {-11, -11, -11, -11, 4}, + {-1, 2, -1, -1, -1}, + {-1, 3, -1, -1, -1}, + }); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 1, 1, 1, true); + assertEquals(4, i); + i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, map, 2, 1, 1, true); + assertEquals(4, i); + } + + /** Transposes the matrix so that we can write it in human-readable format in the tests. */ + private int[][] transpose(int[][] m) { + int[][] t = new int[m[0].length][m.length]; + for (int i = 0; i < m.length; i++) { + for (int j = 0; j < m[0].length; j++) { + t[j][i] = m[i][j]; + } + } + return t; + } } diff --git a/tests/src/com/android/launcher3/util/TestLauncherProvider.java b/tests/src/com/android/launcher3/util/TestLauncherProvider.java new file mode 100644 index 0000000000..8758f55708 --- /dev/null +++ b/tests/src/com/android/launcher3/util/TestLauncherProvider.java @@ -0,0 +1,41 @@ +package com.android.launcher3.util; + +import android.content.Context; + +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherProvider; + +/** + * An extension of LauncherProvider backed up by in-memory database. + */ +public class TestLauncherProvider extends LauncherProvider { + + @Override + public boolean onCreate() { + return true; + } + + @Override + protected synchronized void createDbIfNotExists() { + if (mOpenHelper == null) { + mOpenHelper = new MyDatabaseHelper(getContext()); + } + } + + @Override + protected void notifyListeners() { } + + private static class MyDatabaseHelper extends DatabaseHelper { + public MyDatabaseHelper(Context context) { + super(context, null); + } + + @Override + protected long getDefaultUserSerial() { + return 0; + } + + @Override + protected void onEmptyDbCreated() { } + } +} \ No newline at end of file diff --git a/util/com/android/launcher3/DecoderRing.java b/util/com/android/launcher3/DecoderRing.java deleted file mode 100644 index 0732fe8893..0000000000 --- a/util/com/android/launcher3/DecoderRing.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3; - -import com.android.launcher3.backup.BackupProtos.CheckedMessage; -import com.android.launcher3.backup.BackupProtos.Favorite; -import com.android.launcher3.backup.BackupProtos.Key; -import com.android.launcher3.backup.BackupProtos.Journal; -import com.android.launcher3.backup.BackupProtos.Resource; -import com.android.launcher3.backup.BackupProtos.Screen; -import com.android.launcher3.backup.BackupProtos.Widget; - -import com.google.protobuf.nano.InvalidProtocolBufferNanoException; -import com.google.protobuf.nano.MessageNano; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.System; -import java.util.LinkedList; -import java.util.List; -import java.util.zip.CRC32; - -import javax.xml.bind.DatatypeConverter; - - -/** - * Commandline utility for decoding Launcher3 backup protocol buffers. - * - *

When using com.android.internal.backup.LocalTransport, the file names are base64-encoded Key - * protocol buffers with a prefix, that have been base64-encoded again by the transport: - *

- *     echo "TDpDQUlnL0pxVTVnOD0=" | launcher_protoutil -k
- * 
- * - *

This tool understands these file names and will use the embedded Key to detect the type and - * extract the payload automatically: - *

- *     launcher_protoutil /tmp/TDpDQUlnL0pxVTVnOD0=
- * 
- * - *

With payload debugging enabled, base64-encoded protocol buffers will be written to the logs. - * Copy the encoded snippet from the log, and specify the type explicitly, with the Logs flags: - *

- *    echo "CAEYLiCJ9JKsDw==" | launcher_protoutil -L -k
- * 
- * For backup payloads it is more convenient to copy the log snippet to a file: - *
- *    launcher_protoutil -L -f favorite.log
- * 
- */ -class DecoderRing { - - public static final String STANDARD_IN = "**stdin**"; - - private static Class[] TYPES = { - Key.class, - Favorite.class, - Screen.class, - Resource.class, - Widget.class - }; - static final int ICON_TYPE_BITMAP = 1; - - public static void main(String[ ] args) - throws Exception { - Class defaultType = null; - boolean extractImages = false; - boolean fromLogs = false; - int skip = 0; - List files = new LinkedList(); - boolean verbose = false; - - for (int i = 0; i < args.length; i++) { - if ("-k".equals(args[i])) { - defaultType = Key.class; - } else if ("-f".equals(args[i])) { - defaultType = Favorite.class; - } else if ("-j".equals(args[i])) { - defaultType = Journal.class; - } else if ("-i".equals(args[i])) { - defaultType = Resource.class; - } else if ("-s".equals(args[i])) { - defaultType = Screen.class; - } else if ("-w".equals(args[i])) { - defaultType = Widget.class; - } else if ("-S".equals(args[i])) { - if ((i + 1) < args.length) { - skip = Integer.valueOf(args[++i]); - } else { - usage(args); - } - } else if ("-x".equals(args[i])) { - extractImages = true; - } else if ("-v".equals(args[i])) { - verbose = true; - } else if ("-L".equals(args[i])) { - fromLogs = true; - } else if (args[i] != null && !args[i].startsWith("-")) { - files.add(new File(args[i])); - } else { - System.err.println("Unsupported flag: " + args[i]); - usage(args); - } - } - - if (defaultType == null && files.isEmpty()) { - // can't infer file type without the key - usage(args); - } - - if (files.size() > 1 && defaultType != null) { - System.err.println("Explicit type ignored for multiple files."); - defaultType = null; - } - - if (files.isEmpty()) { - files.add(new File(STANDARD_IN)); - } - - for (File source : files) { - Class type = null; - if (defaultType == null) { - Key key = decodeKey(source.getName().getBytes(), fromLogs); - if (key != null) { - type = TYPES[key.type]; - if (verbose) { - System.err.println(source.getName() + " is a " + type.getSimpleName()); - System.out.println(key.toString()); - } - } - } else { - type = defaultType; - } - - // read in the bytes - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - BufferedInputStream input = null; - if (source.getName() == STANDARD_IN) { - input = new BufferedInputStream(System.in); - } else { - try { - input = new BufferedInputStream(new FileInputStream(source)); - } catch (FileNotFoundException e) { - System.err.println("failed to open file: " + source + ", " + e); - System.exit(1); - } - } - byte[] buffer = new byte[1024]; - try { - while (input.available() > 0) { - int n = input.read(buffer); - int offset = 0; - if (skip > 0) { - offset = Math.min(skip, n); - n -= offset; - skip -= offset; - } - if (n > 0) { - byteStream.write(buffer, offset, n); - } - } - } catch (IOException e) { - System.err.println("failed to read input: " + e); - System.exit(1); - } - - MessageNano proto = null; - byte[] payload = byteStream.toByteArray(); - if (type == Key.class) { - proto = decodeKey(payload, fromLogs); - } else if (type != null) { - proto = decodeBackupData(payload, type, fromLogs); - } - - // Generic string output - if (proto != null) { - System.out.println(proto.toString()); - } - - if (extractImages) { - String prefix = "stdin"; - if (source != null) { - prefix = source.getName(); - } - // save off the icon bits in a file for inspection - if (proto instanceof Resource) { - Resource icon = (Resource) proto; - writeImageData(icon.data, prefix + ".png"); - } - - // save off the icon bits in a file for inspection - if (proto instanceof Favorite) { - Favorite favorite = (Favorite) proto; - if (favorite.iconType == ICON_TYPE_BITMAP) { - writeImageData(favorite.icon, prefix + ".png"); - } - } - - // save off the widget icon and preview bits in files for inspection - if (proto instanceof Widget) { - Widget widget = (Widget) proto; - if (widget.icon != null) { - writeImageData(widget.icon.data, prefix + "_icon.png"); - } - } - } - } - System.exit(0); - } - - // In logcat, backup data is base64 encoded, but in localtransport files it is raw - private static MessageNano decodeBackupData(byte[] payload, Class type, boolean fromLogs) - throws InstantiationException, IllegalAccessException { - MessageNano proto;// other types are wrapped in a checksum message - CheckedMessage wrapper = new CheckedMessage(); - try { - if (fromLogs) { - payload = DatatypeConverter.parseBase64Binary(new String(payload)); - } - MessageNano.mergeFrom(wrapper, payload); - } catch (InvalidProtocolBufferNanoException e) { - System.err.println("failed to parse wrapper: " + e); - System.exit(1); - } - - CRC32 checksum = new CRC32(); - checksum.update(wrapper.payload); - if (wrapper.checksum != checksum.getValue()) { - System.err.println("wrapper checksum failed"); - System.exit(1); - } - - // decode the actual message - proto = (MessageNano) type.newInstance(); - try { - MessageNano.mergeFrom(proto, wrapper.payload); - } catch (InvalidProtocolBufferNanoException e) { - System.err.println("failed to parse proto: " + e); - System.exit(1); - } - return proto; - } - - // In logcat, keys are base64 encoded with no prefix. - // The localtransport adds a prefix and the base64 encodes the whole thing again. - private static Key decodeKey(byte[] payload, boolean fromLogs) { - Key key = new Key(); - try { - String encodedKey = new String(payload); - if (!fromLogs) { - byte[] rawKey = DatatypeConverter.parseBase64Binary(encodedKey); - if (rawKey[0] != 'L' || rawKey[1] != ':') { - System.err.println(encodedKey + " is not a launcher backup key."); - return null; - } - encodedKey = new String(rawKey, 2, rawKey.length - 2); - } - byte[] keyProtoData = DatatypeConverter.parseBase64Binary(encodedKey); - key = Key.parseFrom(keyProtoData); - } catch (InvalidProtocolBufferNanoException protoException) { - System.err.println("failed to extract key from filename: " + protoException); - return null; - } catch (IllegalArgumentException base64Exception) { - System.err.println("failed to extract key from filename: " + base64Exception); - return null; - } - - // keys are self-checked - if (key.checksum != checkKey(key)) { - System.err.println("key ckecksum failed"); - return null; - } - return key; - } - - private static void writeImageData(byte[] data, String path) { - FileOutputStream iconFile = null; - try { - iconFile = new FileOutputStream(path); - iconFile.write(data); - System.err.println("wrote " + path); - } catch (IOException e) { - System.err.println("failed to write image file: " + e); - } finally { - if (iconFile != null) { - try { - iconFile.close(); - } catch (IOException e) { - System.err.println("failed to close the image file: " + e); - } - } - } - } - - private static long checkKey(Key key) { - CRC32 checksum = new CRC32(); - checksum.update(key.type); - checksum.update((int) (key.id & 0xffff)); - checksum.update((int) ((key.id >> 32) & 0xffff)); - if (key.name != null && key.name.length() > 0) { - checksum.update(key.name.getBytes()); - } - return checksum.getValue(); - } - - private static void usage(String[] args) { - System.err.println("launcher_protoutil [-x] [-S b] [-k|-f|-i|-s|-w] [filename]"); - System.err.println("\t-k\tdecode a key"); - System.err.println("\t-f\tdecode a favorite"); - System.err.println("\t-i\tdecode a icon"); - System.err.println("\t-s\tdecode a screen"); - System.err.println("\t-w\tdecode a widget"); - System.err.println("\t-S b\tskip b bytes"); - System.err.println("\t-x\textract image data to files"); - System.err.println("\t-v\tprint key type data, as well as payload"); - System.err.println("\t-l\texpect data from logcat, instead of the local transport"); - System.err.println("\tfilename\tread from filename, not stdin"); - System.exit(1); - } -} \ No newline at end of file diff --git a/util/etc/launcher_protoutil b/util/etc/launcher_protoutil deleted file mode 100644 index 833b5835e8..0000000000 --- a/util/etc/launcher_protoutil +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2013 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. - -# Set up prog to be the path of this script, including following symlinks, -# and set up progdir to be the fully-qualified pathname of its directory. -prog="$0" -while [ -h "${prog}" ]; do - newProg=`/bin/ls -ld "${prog}"` - newProg=`expr "${newProg}" : ".* -> \(.*\)$"` - if expr "x${newProg}" : 'x/' >/dev/null; then - prog="${newProg}" - else - progdir=`dirname "${prog}"` - prog="${progdir}/${newProg}" - fi -done -oldwd=`pwd` -progdir=`dirname "${prog}"` -cd "${progdir}" -progdir=`pwd` -prog="${progdir}"/`basename "${prog}"` -cd "${oldwd}" - -jarfile=launcher_protoutil_lib.jar -libdir="$progdir" - -if [ ! -r "$libdir/$jarfile" ]; then - # set jar location for the Android tree case - libdir=`dirname "$progdir"`/framework -fi - -if [ ! -r "$libdir/$jarfile" ]; then - echo `basename "$prog"`": can't find $jarfile" - exit 1 -fi - -# By default, give decoder a max heap size of 1 gig. This can be overridden -# by using a "-J" option (see below). -defaultMx="-Xmx1024M" - -# The following will extract any initial parameters of the form -# "-J" from the command line and pass them to the Java -# invocation (instead of to the decoder). This makes it possible for -# you to add a command-line parameter such as "-JXmx256M" in your -# scripts, for example. "java" (with no args) and "java -X" give a -# summary of available options. - -javaOpts="" - -while expr "x$1" : 'x-J' >/dev/null; do - opt=`expr "x$1" : 'x-J\(.*\)'` - javaOpts="${javaOpts} -${opt}" - if expr "x${opt}" : "xXmx[0-9]" >/dev/null; then - defaultMx="no" - fi - shift -done - -if [ "${defaultMx}" != "no" ]; then - javaOpts="${javaOpts} ${defaultMx}" -fi - -if [ "$OSTYPE" = "cygwin" ]; then - # For Cygwin, convert the jarfile path into native Windows style. - jarpath=`cygpath -w "$libdir/$jarfile"` -else - jarpath="$libdir/$jarfile" -fi - -exec java $javaOpts -jar "$jarpath" "$@" diff --git a/util/etc/manifest.txt b/util/etc/manifest.txt deleted file mode 100644 index 84842ed539..0000000000 --- a/util/etc/manifest.txt +++ /dev/null @@ -1 +0,0 @@ -Main-Class: com.android.launcher3.DecoderRing