diff options
Diffstat (limited to 'java')
191 files changed, 6840 insertions, 1930 deletions
diff --git a/java/res/drawable/checkbox.xml b/java/res/drawable/checkbox.xml new file mode 100644 index 00000000..189d01ff --- /dev/null +++ b/java/res/drawable/checkbox.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="20dp" +    android:height="20dp" +    android:viewportWidth="20" +    android:viewportHeight="20"> +  <path +      android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM10,18C5.59,18 2,14.41 2,10C2,5.59 5.59,2 10,2C14.41,2 18,5.59 18,10C18,14.41 14.41,18 10,18ZM5.4,9.6L8,12.2L14.6,5.6L16,7L8,15L4,11L5.4,9.6Z" +      android:fillColor="#ffffff" +      android:fillType="evenOdd"/> +</vector> diff --git a/java/res/drawable/ic_play_circle_filled_24px.xml b/java/res/drawable/ic_play_circle_filled_24px.xml new file mode 100644 index 00000000..f67127ca --- /dev/null +++ b/java/res/drawable/ic_play_circle_filled_24px.xml @@ -0,0 +1,3 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="20dp" android:viewportHeight="20" android:viewportWidth="20" android:width="20dp"> +    <path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M0,10C0,4.48 4.48,0 10,0C15.52,0 20,4.48 20,10C20,15.52 15.52,20 10,20C4.48,20 0,15.52 0,10ZM14,10L8,5.5V14.5L14,10Z"/> +</vector> diff --git a/java/res/layout/chooser_action_view.xml b/java/res/layout/chooser_action_view.xml index e17dce0e..d045a7e3 100644 --- a/java/res/layout/chooser_action_view.xml +++ b/java/res/layout/chooser_action_view.xml @@ -27,6 +27,5 @@      android:ellipsize="end"      android:gravity="center"      android:maxLines="1" -    android:maxWidth="@dimen/chooser_action_max_width"      android:textColor="?androidprv:attr/materialColorOnSurface"      android:textSize="12sp" /> diff --git a/java/res/layout/chooser_grid_item.xml b/java/res/layout/chooser_grid_item.xml new file mode 100644 index 00000000..18abc7bc --- /dev/null +++ b/java/res/layout/chooser_grid_item.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2006, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +**     http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +              xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" +              android:id="@androidprv:id/item" +              android:orientation="vertical" +              android:layout_width="match_parent" +              android:layout_height="wrap_content" +              android:minHeight="100dp" +              android:gravity="top|center_horizontal" +              android:paddingVertical="@dimen/grid_padding" +              android:paddingHorizontal="4dp" +              android:focusable="true" +              android:background="?android:attr/selectableItemBackgroundBorderless"> + +    <ImageView android:id="@android:id/icon" +               android:layout_width="@dimen/chooser_icon_size" +               android:layout_height="@dimen/chooser_icon_size" +               android:layout_marginHorizontal="8dp" +               android:scaleType="fitCenter" /> + +    <!-- Size manually tuned to match specs --> +    <Space android:layout_width="1dp" +           android:layout_height="7dp"/> + +    <!-- NOTE: for id/text1 and id/text2 below set the width to match parent as a workaround for +         b/269395540 i.e. prevent views bounds change during a transition animation. It does not +         affect pinned views as we change their layout parameters programmatically (but that's even +         more narrow possibility and it's not clear if the root cause or the bug would affect it). +    --> +    <!-- App name or Direct Share target name, DS set to 2 lines --> +    <com.android.intentresolver.widget.BadgeTextView +              android:id="@android:id/text1" +              android:layout_width="match_parent" +              android:layout_height="wrap_content" +              android:textAppearance="?android:attr/textAppearanceSmall" +              android:textColor="?androidprv:attr/materialColorOnSurface" +              android:textSize="12sp" +              android:maxLines="1" +              android:ellipsize="end" /> + +    <!-- Activity name if set, gone for Direct Share targets --> +    <TextView android:id="@android:id/text2" +              android:textAppearance="?android:attr/textAppearanceSmall" +              android:textSize="12sp" +              android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" +              android:layout_width="match_parent" +              android:layout_height="wrap_content" +              android:lines="1" +              android:gravity="top|center_horizontal" +              android:ellipsize="end"/> + +</LinearLayout> + diff --git a/java/res/layout/chooser_headline_row.xml b/java/res/layout/chooser_headline_row.xml index 62781847..97e8552e 100644 --- a/java/res/layout/chooser_headline_row.xml +++ b/java/res/layout/chooser_headline_row.xml @@ -38,6 +38,21 @@          android:textSize="18sp"          /> +    <TextView +        android:id="@+id/metadata" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:visibility="gone" +        app:layout_constraintStart_toStartOf="parent" +        app:layout_constraintEnd_toStartOf="@id/barrier" +        app:layout_constraintHorizontal_bias="0.0" +        app:layout_constrainedWidth="true" +        app:layout_constraintTop_toBottomOf="@id/headline" +        style="@style/TextAppearance.ChooserDefault" +        android:fontFamily="@androidprv:string/config_bodyFontFamily" +        android:textSize="12sp" +        /> +      <androidx.constraintlayout.widget.Barrier          android:id="@+id/barrier"          android:layout_width="wrap_content" diff --git a/java/res/layout/chooser_list_per_profile_wrap.xml b/java/res/layout/chooser_list_per_profile_wrap.xml index 157fa75d..fc0431d7 100644 --- a/java/res/layout/chooser_list_per_profile_wrap.xml +++ b/java/res/layout/chooser_list_per_profile_wrap.xml @@ -35,7 +35,6 @@          android:clipToPadding="false"          android:background="?androidprv:attr/materialColorSurfaceContainer"          android:scrollbars="none" -        android:elevation="1dp"          android:nestedScrollingEnabled="true" />      <include layout="@layout/resolver_empty_states" /> diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml index e0a73836..4ce4e614 100644 --- a/java/res/values-af/strings.xml +++ b/java/res/values-af/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deel tans video met skakel}other{Deel tans # video’s met skakel}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deel tans lêer met teks}other{Deel tans # lêers met teks}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deel tans lêer met skakel}other{Deel tans # lêers met skakel}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deel tans album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Net prent}other{Net prente}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Net video}other{Net video’s}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Net lêer}other{Net lêers}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Opneemtoestemming is nie aan hierdie program verleen nie, maar dit kan oudio deur hierdie USB-toestel opneem."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Persoonlik"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Werk"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privaat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persoonlike aansig"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Werkaansig"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privaat aansig"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Deur jou IT-admin geblokkeer"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Hierdie inhoud kan nie met werkprogramme gedeel word nie"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hierdie inhoud kan nie met werkprogramme oopgemaak word nie"</string> diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml index ba6409fd..e8c5a033 100644 --- a/java/res/values-am/strings.xml +++ b/java/res/values-am/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ቪድዮ ከአገናኝ ጋር በማጋራት ላይ}one{# ቪድዮ ከአገናኝ ጋር በማጋራት ላይ}other{# ቪድዮዎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ፋይልን ከጽሑፍ ጋር በማጋራት ላይ}one{# ፋይልን ከጽሑፍ ጋር በማጋራት ላይ}other{# ፋይሎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ፋይልን ከአገናኝ ጋር በማጋራት ላይ}one{# ፋይልን ከአገናኝ ጋር በማጋራት ላይ}other{# ፋይሎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"የተጋራ አልበም"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ምስል ብቻ}one{ምስል ብቻ}other{ምስሎች ብቻ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ቪድዮ ብቻ}one{ቪድዮ ብቻ}other{ቪድዮዎች ብቻ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ፋይል ብቻ}one{ፋይል ብቻ}other{ፋይሎች ብቻ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ይህ መተግበሪያ የመቅረጽ ፈቃድ አልተሰጠውም፣ ነገር ግን በዚህ ዩኤስቢ መሣሪያ በኩል ኦዲዮን መቅረጽ ይችላል።"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"የግል"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ሥራ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"የግል"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"የግል ዕይታ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ ዕይታ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"የግል ዕይታ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"በእርስዎ የአይቲ አስተዳዳሪ ታግዷል"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ይህ ይዘት በሥራ መተግበሪያዎች መጋራት አይችልም"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ይህ ይዘት በሥራ መተግበሪያዎች መከፈት አይችልም"</string> diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml index da8d4de2..a5979327 100644 --- a/java/res/values-ar/strings.xml +++ b/java/res/values-ar/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{مشاركة فيديو واحد ورابط}zero{مشاركة # فيديو ورابط}two{مشاركة فيديوهَين ورابط}few{مشاركة # فيديوهات ورابط}many{مشاركة # فيديو ورابط}other{مشاركة # فيديو ورابط}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{مشاركة ملف واحد ونص}zero{مشاركة # ملف ونص}two{مشاركة # ملفَّين ونص}few{مشاركة # ملفات ونص}many{مشاركة # ملفًا ونص}other{مشاركة # ملف ونص}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{مشاركة ملف واحد ورابط}zero{مشاركة # ملف ورابط}two{مشاركة ملفَّين ورابط}few{مشاركة # ملفات ورابط}many{مشاركة # ملفًا ورابط}other{مشاركة # ملف ورابط}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"مشاركة الألبوم"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{الصورة فقط}zero{الصور فقط}two{الصورتان فقط}few{الصور فقط}many{الصور فقط}other{الصور فقط}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{الفيديو فقط}zero{الفيديوهات فقط}two{الفيديوهان فقط}few{الفيديوهات فقط}many{الفيديوهات فقط}other{الفيديوهات فقط}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{الملف فقط}zero{الملفات فقط}two{الملفان فقط}few{الملفات فقط}many{الملفات فقط}other{الملفات فقط}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"لم يتم منح هذا التطبيق إذن تسجيل، ولكن يمكنه تسجيل الصوت من خلال جهاز USB هذا."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"شخصي"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"للعمل"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"المساحة الخاصّة"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"عرض المحتوى الشخصي"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"عرض محتوى العمل"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"عرض المساحة الخاصّة"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"حظر مشرف تكنولوجيا المعلومات مشاركة المحتوى"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"لا يمكن مشاركة هذا المحتوى مع تطبيقات العمل."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"لا يمكن فتح هذا المحتوى باستخدام تطبيقات العمل."</string> diff --git a/java/res/values-as/strings.xml b/java/res/values-as/strings.xml index 14bd864e..dd677968 100644 --- a/java/res/values-as/strings.xml +++ b/java/res/values-as/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{লিংকৰ সৈতে ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{পাঠৰ সৈতে ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{লিংকৰ সৈতে ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"এলবাম শ্বেয়াৰ কৰি থকা হৈছে"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{কেৱল প্ৰতিচ্ছবি}one{কেৱল প্ৰতিচ্ছবিসমূহ}other{কেৱল প্ৰতিচ্ছবিসমূহ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{কেৱল ভিডিঅ’}one{কেৱল ভিডিঅ’সমূহ}other{কেৱল ভিডিঅ’সমূহ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{কেৱল ফাইল}one{কেৱল ফাইলসমূহ}other{কেৱল ফাইলসমূহ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"এই এপ্টোক ৰেকর্ড কৰাৰ অনুমতি দিয়া হোৱা নাই কিন্তু ই এই ইউএছবি ডিভাইচটোৰ জৰিয়তে অডিঅ\' ৰেকর্ড কৰিব পাৰে।"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ব্যক্তিগত"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"কৰ্মস্থান"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ব্যক্তিগত"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ব্যক্তিগত ভিউ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"কৰ্মস্থানৰ ভিউ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ব্যক্তিগত ভিউ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"আপোনাৰ আইটি প্ৰশাসকে অৱৰোধ কৰিছে"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"এই সমল কৰ্মস্থানৰ এপৰ সৈতে শ্বেয়াৰ কৰিব নোৱাৰি"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"এই সমল কৰ্মস্থানৰ এপৰ জৰিয়তে খুলিব নোৱাৰি"</string> diff --git a/java/res/values-az/strings.xml b/java/res/values-az/strings.xml index a31df362..39e396d0 100644 --- a/java/res/values-az/strings.xml +++ b/java/res/values-az/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Link olan video paylaşılır}other{Link olan # video paylaşılır}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Mətn olan fayl paylaşılır}other{Mətn olan # fayl paylaşılır}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Link olan fayl paylaşılır}other{Link olan # fayl paylaşılır}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albom paylaşılır"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Yalnız şəkil}other{Yalnız şəkillər}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Yalnız video}other{Yalnız videolar}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Yalnız fayl}other{Yalnız fayllar}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tətbiqə qeydə almaq icazəsi verilməsə də, bu USB vasitəsilə səsi qeydə ala bilər."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Şəxsi"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"İş"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Şəxsi"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Şəxsi məzmuna baxış"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"İş məzmununa baxış"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Şəxsi baxış"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT admininiz tərəfindən bloklanıb"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu kontenti iş tətbiqləri ilə paylaşmaq mümkün deyil"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontenti iş tətbiqləri ilə açmaq mümkün deyil"</string> diff --git a/java/res/values-b+sr+Latn/strings.xml b/java/res/values-b+sr+Latn/strings.xml index ea0d87b3..91dd0c13 100644 --- a/java/res/values-b+sr+Latn/strings.xml +++ b/java/res/values-b+sr+Latn/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ još # fajl}one{+ još # fajl}few{+ još # fajla}other{+ još # fajlova}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deli se tekst"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deli se link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deli se slika}one{Deli se # slika}few{Dele se # slike}other{Deli se # slika}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deljenje slike}one{Deljenje # slike}few{Deljenje # slike}other{Deljenje # slika}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deli se video}one{Deli se # video}few{Dele se # video snimka}other{Deli se # videa}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deli se # fajl}one{Deli se # fajl}few{Dele se # fajla}other{Deli se # fajlova}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deli se slika sa tekstom}one{Deli se # slika sa tekstom}few{Dele se # slike sa tekstom}other{Deli se # slika sa tekstom}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deli se video sa linkom}one{Deli se # video sa linkom}few{Dele se # video snimka sa linkom}other{Deli se # videa sa linkom}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deli se fajl sa tekstom}one{Deli se # fajl sa tekstom}few{Dele se # fajla sa tekstom}other{Deli se # fajlova sa tekstom}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deli se fajl sa linkom}one{Deli se # fajl sa linkom}few{Dele se # fajla sa linkom}other{Deli se # fajlova sa linkom}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deljeni album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo video}one{Samo video snimci}few{Samo video snimci}other{Samo video snimci}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo fajl}one{Samo fajlovi}few{Samo fajlovi}other{Samo fajlovi}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ova aplikacija nema dozvolu za snimanje, ali bi mogla da snima zvuk pomoću ovog USB uređaja."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Lično"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Poslovno"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Lični prikaz"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Prikaz za posao"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatni prikaz"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokira IT administrator"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ovaj sadržaj ne može da se deli pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj ne može da se otvara pomoću poslovnih aplikacija"</string> diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml index aecc1cbd..2f5d8004 100644 --- a/java/res/values-be/strings.xml +++ b/java/res/values-be/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Абагульванне відэа са спасылкай}one{Абагульванне # відэа са спасылкай}few{Абагульванне # відэа са спасылкай}many{Абагульванне # відэа са спасылкай}other{Абагульванне # відэа са спасылкай}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Абагульванне файла з тэкстам}one{Абагульванне # файла з тэкстам}few{Абагульванне # файлаў з тэкстам}many{Абагульванне # файлаў з тэкстам}other{Абагульванне # файла з тэкстам}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Абагульванне файла са спасылкай}one{Абагульванне # файла са спасылкай}few{Абагульванне # файлаў са спасылкай}many{Абагульванне # файлаў са спасылкай}other{Абагульванне # файла са спасылкай}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Ідзе абагульванне альбома"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Толькі відарыс}one{Толькі відарысы}few{Толькі відарысы}many{Толькі відарысы}other{Толькі відарысы}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Толькі відэа}one{Толькі відэа}few{Толькі відэа}many{Толькі відэа}other{Толькі відэа}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Толькі файл}one{Толькі файлы}few{Толькі файлы}many{Толькі файлы}other{Толькі файлы}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"У гэтай праграмы няма дазволу на запіс, аднак яна зможа запісваць аўдыя праз гэту USB-прыладу."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Асабісты"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Працоўны"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Прыватная прастора"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Прагляд асабістага змесціва"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Прагляд працоўнага змесціва"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Прыватная прастора"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Заблакіравана вашым ІТ-адміністратарам"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Не ўдалося абагуліць гэта змесціва з працоўнымі праграмамі"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Не ўдалося адкрыць гэта змесціва з дапамогай працоўных праграм"</string> diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml index 5bc22d73..2736cb13 100644 --- a/java/res/values-bg/strings.xml +++ b/java/res/values-bg/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Споделяне на видеоклипа чрез връзка}other{Споделяне на # видеоклипа чрез връзка}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Споделяне на файла чрез SMS съобщение}other{Споделяне на # файла чрез SMS съобщение}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Споделяне на файла чрез връзка}other{Споделяне на # файла чрез връзка}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Споделяне на албума"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само изображение}other{Само изображения}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видеоклип}other{Само видеоклипове}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само файл}other{Само файлове}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Приложението няма разрешение за записване, но може да записва звук чрез това USB устройство."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Лични"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Служебни"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Частно"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Личен изглед"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Служебен изглед"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Частен изглед"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Блокирано от системния ви администратор"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Това съдържание не може да се споделя със служебни приложения"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Това съдържание не може да се отваря със служебни приложения"</string> diff --git a/java/res/values-bn/strings.xml b/java/res/values-bn/strings.xml index 0561cf99..db656a0d 100644 --- a/java/res/values-bn/strings.xml +++ b/java/res/values-bn/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{লিঙ্ক সহ ভিডিও শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ভিডিও শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ভিডিও শেয়ার করা হচ্ছে}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{টেক্সট সহ ফাইল শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ফাইল শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ফাইল শেয়ার করা হচ্ছে}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{লিঙ্ক সহ ফাইল শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ফাইল শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ফাইল শেয়ার করা হচ্ছে}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"অ্যালবাম শেয়ার করা হচ্ছে"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{শুধু ছবি}one{শুধু ছবি}other{শুধু ছবি}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{শুধু ভিডিও}one{শুধু ভিডিও}other{শুধু ভিডিও}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{শুধু ফাইল}one{শুধু ফাইল}other{শুধু ফাইল}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"এই অ্যাপকে রেকর্ড করার অনুমতি দেওয়া হয়নি কিন্তু USB ডিভাইসের মাধ্যমে সেটি অডিও রেকর্ড করতে পারে।"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ব্যক্তিগত"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"অফিস"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ব্যক্তিগত"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ব্যক্তিগত ভিউ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"অফিসের ভিউ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ব্যক্তিগত ভিউ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"আপনার আইটি অ্যাডমিন ব্লক করেছেন"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"অফিসের অ্যাপে এই কন্টেন্ট শেয়ার করা যাবে না"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"অফিসের অ্যাপে এই খোলা যাবে না"</string> diff --git a/java/res/values-bs/strings.xml b/java/res/values-bs/strings.xml index 3c88d9c1..d5f76fe7 100644 --- a/java/res/values-bs/strings.xml +++ b/java/res/values-bs/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Dijeljenje videozapisa putem linka}one{Dijeljenje # videozapisa putem linka}few{Dijeljenje # videozapisa putem linka}other{Dijeljenje # videozapisa putem linka}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Dijeljenje fajla putem poruke}one{Dijeljenje # fajla putem poruke}few{Dijeljenje # fajla putem poruke}other{Dijeljenje # fajlova putem poruke}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Dijeljenje fajla putem linka}one{Dijeljenje # fajla putem linka}few{Dijeljenje # fajla putem linka}other{Dijeljenje # fajlova putem linka}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Dijeljenje albuma"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videozapis}one{Samo videozapisi}few{Samo videozapisi}other{Samo videozapisi}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo fajl}one{Samo fajlovi}few{Samo fajlovi}other{Samo fajlovi}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ovoj aplikaciji nije dato odobrenje za snimanje, ali može snimati zvuk putem ovog USB uređaja."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Lično"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Posao"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Prikaz ličnog sadržaja"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Prikaz poslovnog sadržaja"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatan prikaz"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokirao je vaš IT administrator"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ovaj sadržaj nije moguće dijeliti pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj nije moguće otvoriti pomoću poslovnih aplikacija"</string> diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml index bd0416a5..a3407873 100644 --- a/java/res/values-ca/strings.xml +++ b/java/res/values-ca/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# fitxer més}many{# de fitxers més}other{# fitxers més}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"S\'està compartint text"</string>      <string name="sharing_link" msgid="2307694372813942916">"S\'està compartint un enllaç"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{S\'està compartint una imatge}many{S\'estan compartint # d\'imatges}other{S\'estan compartint # imatges}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Comparteix una imatge}many{Comparteix # d\'imatges}other{Comparteix # imatges}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{S\'està compartint un vídeo}many{S\'estan compartint # de vídeos}other{S\'estan compartint # vídeos}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{S\'està compartint # fitxer}many{S\'estan compartint # de fitxers}other{S\'estan compartint # fitxers}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{S\'està compartint la imatge amb text}many{S\'estan compartint # d\'imatges amb text}other{S\'estan compartint # imatges amb text}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{S\'està compartint el vídeo amb un enllaç}many{S\'estan compartint # de vídeos amb un enllaç}other{S\'estan compartint # vídeos amb un enllaç}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{S\'està compartint el fitxer amb text}many{S\'estan compartint # de fitxers amb text}other{S\'estan compartint # fitxers amb text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{S\'està compartint el fitxer amb un enllaç}many{S\'estan compartint # de fitxers amb un enllaç}other{S\'estan compartint # fitxers amb un enllaç}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"S\'està compartint l\'àlbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Només imatge}many{Només imatges}other{Només imatges}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Només vídeo}many{Només vídeos}other{Només vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Només fitxer}many{Només fitxers}other{Només fitxers}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aquesta aplicació no té permís de gravació, però pot capturar àudio a través d\'aquest dispositiu USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Feina"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualització personal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualització de treball"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualització privada"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloquejat per l\'administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"No es pot compartir aquest contingut amb aplicacions de treball"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No es pot obrir aquest contingut amb aplicacions de treball"</string> diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml index a5deed60..93712487 100644 --- a/java/res/values-cs/strings.xml +++ b/java/res/values-cs/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sdílení videa s odkazem}few{Sdílení # videí s odkazem}many{Sdílení # videa s odkazem}other{Sdílení # videí s odkazem}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sdílení souboru s textem}few{Sdílení # souborů s textem}many{Sdílení # souboru s textem}other{Sdílení # souborů s textem}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sdílení souboru s odkazem}few{Sdílení # souborů s odkazem}many{Sdílení # souboru s odkazem}other{Sdílení # souborů s odkazem}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sdílení alba"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Pouze obrázek}few{Pouze obrázky}many{Pouze obrázky}other{Pouze obrázky}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Pouze video}few{Pouze videa}many{Pouze videa}other{Pouze videa}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Pouze soubor}few{Pouze soubory}many{Pouze soubory}other{Pouze soubory}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tato aplikace nemá oprávnění k nahrávání, ale může zaznamenávat zvuk prostřednictvím tohoto zařízení USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Osobní"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Pracovní"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Soukromé"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobní zobrazení"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pracovní zobrazení"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Soukromé zobrazení"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokováno administrátorem IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tento obsah nelze sdílet pomocí pracovních aplikací"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah nelze otevřít pomocí pracovních aplikací"</string> diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml index 8d226d44..26385908 100644 --- a/java/res/values-da/strings.xml +++ b/java/res/values-da/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deler video med et link}one{Deler # video med et link}other{Deler # videoer med et link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deler fil med tekst}one{Deler # fil med tekst}other{Deler # filer med tekst}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deler fil med et link}one{Deler # fil med et link}other{Deler # filer med et link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deling af albummet"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Kun billedet}one{Kun billedet}other{Kun billeder}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Kun video}one{Kun video}other{Kun videoer}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Kun filen}one{Kun filen}other{Kun filer}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Denne app har ikke fået tilladelse til at optage, men optager muligvis lyd via denne USB-enhed."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personlig"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Arbejde"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visningen Personligt"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visningen Arbejde"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visningen Privat"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokeret af din it-administrator"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Dette indhold kan ikke deles med arbejdsapps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette indhold kan ikke åbnes med arbejdsapps"</string> diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml index dc476fa7..67ea5e63 100644 --- a/java/res/values-de/strings.xml +++ b/java/res/values-de/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Video wird per Link geteilt}other{# Videos werden per Link geteilt}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Datei wird per SMS geteilt}other{# Dateien werden per SMS geteilt}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Datei wird per Link geteilt}other{# Dateien werden per Link geteilt}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Album teilen"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Nur Bild}other{Nur Bilder}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Nur Video}other{Nur Videos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Nur Datei}other{Nur Dateien}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Diese App hat noch keine Berechtigung zum Aufnehmen erhalten, könnte aber Audioaufnahmen über dieses USB-Gerät machen."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Privat"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Geschäftlich"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Private Ansicht"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Geschäftliche Ansicht"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private Ansicht"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Von deinem IT-Administrator blockiert"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Diese Art von Inhalt kann nicht über geschäftliche Apps geteilt werden"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Diese Art von Inhalt kann nicht mit geschäftlichen Apps geöffnet werden"</string> diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml index e760e00c..f88dc751 100644 --- a/java/res/values-el/strings.xml +++ b/java/res/values-el/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Κοινοποίηση βίντεο με σύνδεσμο}other{Κοινοποίηση # βίντεο με σύνδεσμο}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Κοινοποίηση αρχείου με κείμενο}other{Κοινοποίηση # αρχείων με κείμενο}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Κοινοποίηση αρχείου με σύνδεσμο}other{Κοινοποίηση # αρχείων με σύνδεσμο}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Κοινοποίηση λευκώματος"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Μόνο εικόνα}other{Μόνο εικόνες}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Μόνο βίντεο}other{Μόνο βίντεο}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Μόνο αρχείο}other{Μόνο αρχεία}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Δεν έχει εκχωρηθεί άδεια εγγραφής σε αυτή την εφαρμογή, αλλά μέσω αυτής της συσκευής USB θα μπορεί να εγγράφει ήχο."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Προσωπικό"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Εργασία"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Ιδιωτική"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Προσωπική προβολή"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Προβολή εργασίας"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ιδιωτική προβολή"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Αποκλείστηκε από τον διαχειριστή IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Δεν είναι δυνατή η κοινοποίηση αυτού του περιεχομένου με εφαρμογές εργασιών"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Δεν είναι δυνατό το άνοιγμα αυτού του περιεχομένου με εφαρμογές εργασιών"</string> diff --git a/java/res/values-en-rAU/strings.xml b/java/res/values-en-rAU/strings.xml index a1438ed9..90f6974a 100644 --- a/java/res/values-en-rAU/strings.xml +++ b/java/res/values-en-rAU/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sharing album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string> diff --git a/java/res/values-en-rCA/strings.xml b/java/res/values-en-rCA/strings.xml index a1438ed9..90f6974a 100644 --- a/java/res/values-en-rCA/strings.xml +++ b/java/res/values-en-rCA/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sharing album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string> diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml index a1438ed9..90f6974a 100644 --- a/java/res/values-en-rGB/strings.xml +++ b/java/res/values-en-rGB/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sharing album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string> diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml index a1438ed9..90f6974a 100644 --- a/java/res/values-en-rIN/strings.xml +++ b/java/res/values-en-rIN/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sharing album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string> diff --git a/java/res/values-en-rXC/strings.xml b/java/res/values-en-rXC/strings.xml index 56574b6c..f0650785 100644 --- a/java/res/values-en-rXC/strings.xml +++ b/java/res/values-en-rXC/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Sharing album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string> diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml index 97ae9a6c..e79f383b 100644 --- a/java/res/values-es-rUS/strings.xml +++ b/java/res/values-es-rUS/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartir video con vínculo}many{Compartir # de videos con vínculo}other{Compartir # videos con vínculo}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartir archivo con texto}many{Compartir # de archivos con texto}other{Compartir # archivos con texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartir archivo con vínculo}many{Compartir # de archivos con vínculo}other{Compartir # archivos con vínculo}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Se comparte este álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Solo imagen}many{Solo imágenes}other{Solo imágenes}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Solo video}many{Solo videos}other{Solo videos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Solo archivo}many{Solo archivos}other{Solo archivos}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aunque no se le otorgó permiso de grabación a esta app, puede capturar audio con este dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabajo"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista personal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de trabajo"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado por tu administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"No se pueden usar apps de trabajo para compartir este contenido"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No se puede abrir este contenido con apps de trabajo"</string> diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml index 0c42bb82..28b37a73 100644 --- a/java/res/values-es/strings.xml +++ b/java/res/values-es/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{y # archivo más}many{y # archivos más}other{y # archivos más}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Compartiendo texto"</string>      <string name="sharing_link" msgid="2307694372813942916">"Compartiendo enlace"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartiendo imagen}many{Compartiendo # imágenes}other{Compartiendo # imágenes}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartir imagen}many{Compartir # imágenes}other{Compartir # imágenes}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartiendo vídeo}many{Compartiendo # vídeos}other{Compartiendo # vídeos}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartiendo # archivo}many{Compartiendo # archivos}other{Compartiendo # archivos}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartiendo imagen con texto}many{Compartiendo # imágenes con texto}other{Compartiendo # imágenes con texto}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartiendo vídeo con enlace}many{Compartiendo # vídeos con enlace}other{Compartiendo # vídeos con enlace}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartiendo archivo con mensaje de texto}many{Compartiendo # archivos con mensaje de texto}other{Compartiendo # archivos con mensaje de texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartiendo archivo con enlace}many{Compartiendo # archivos con enlace}other{Compartiendo # archivos con enlace}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Compartiendo álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Solo imagen}many{Solo imágenes}other{Solo imágenes}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Solo vídeo}many{Solo vídeos}other{Solo vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Solo archivo}many{Solo archivos}other{Solo archivos}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta aplicación no tiene permiso para grabar, pero podría capturar audio con este dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabajo"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ver contenido personal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ver contenido de trabajo"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado por tu administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Este contenido no se puede compartir con aplicaciones de trabajo"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contenido no se puede abrir con aplicaciones de trabajo"</string> @@ -95,5 +98,5 @@      <string name="include_text" msgid="642280283268536140">"Incluir texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluir enlace"</string>      <string name="include_link" msgid="827855767220339802">"Incluir enlace"</string> -    <string name="pinned" msgid="7623664001331394139">"Fijada"</string> +    <string name="pinned" msgid="7623664001331394139">"Fijado"</string>  </resources> diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml index bc960699..9b4fff07 100644 --- a/java/res/values-et/strings.xml +++ b/java/res/values-et/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Linki sisaldava video jagamine}other{# linki sisaldava video jagamine}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Teksti sisaldava faili jagamine}other{# teksti sisaldava faili jagamine}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Linki sisaldava faili jagamine}other{# linki sisaldava faili jagamine}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albumi jagamine"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Ainult pilt}other{Ainult pildid}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Ainult video}other{Ainult videod}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Ainult fail}other{Ainult failid}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Sellele rakendusele pole antud salvestamise luba, kuid see saab heli jäädvustada selle USB-seadme kaudu."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Isiklik"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Töö"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privaatne"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Isiklik vaade"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Töövaade"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privaatne vaade"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokeeris teie IT-administraator"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Seda sisu ei saa töörakendustega jagada"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Seda sisu ei saa töörakendustega avada"</string> diff --git a/java/res/values-eu/strings.xml b/java/res/values-eu/strings.xml index 1cc7576b..275eb854 100644 --- a/java/res/values-eu/strings.xml +++ b/java/res/values-eu/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bideo estekadun bat partekatuko da}other{# bideo estekadun partekatuko dira}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Fitxategi testudun bat partekatuko da}other{# fitxategi testudun partekatuko dira}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Fitxategi estekadun bat partekatuko da}other{# fitxategi estekadun partekatuko dira}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albuma partekatzea"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Irudia soilik}other{Irudiak bakarrik}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Bideoa soilik}other{Bideoak soilik}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fitxategia soilik}other{Fitxategiak soilik}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aplikazioak ez du grabatzeko baimenik, baina baliteke audioa grabatzea USB bidezko gailu horren bidez."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Pertsonala"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Lanekoa"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Pribatua"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ikuspegi pertsonala"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Laneko ikuspegia"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ikuspegi pribatua"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IKT saileko administratzaileak blokeatu egin du"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Eduki hau ezin da laneko aplikazioekin partekatu"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Eduki hau ezin da laneko aplikazioekin ireki"</string> diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml index 58313f70..82a75582 100644 --- a/java/res/values-fa/strings.xml +++ b/java/res/values-fa/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{درحال همرسانی ویدیو با پیوند}one{درحال همرسانی # ویدیو با پیوند}other{درحال همرسانی # ویدیو با پیوند}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{درحال همرسانی فایل با نوشتار}one{درحال همرسانی # فایل با نوشتار}other{درحال همرسانی # فایل با نوشتار}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{درحال همرسانی فایل با پیوند}one{درحال همرسانی # فایل با پیوند}other{درحال همرسانی # فایل با پیوند}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"همرسانی آلبوم"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{فقط تصویر}one{فقط تصویر}other{فقط تصویر}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{فقط ویدیو}one{فقط ویدیو}other{فقط ویدیو}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{فقط فایل}one{فقط فایل}other{فقط فایل}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"مجوز ضبط به این برنامه داده نشده است اما میتواند صدا را ازطریق این دستگاه USB ضبط کند."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"شخصی"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"کاری"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"خصوصی"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"نمای شخصی"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"نمای کاری"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"نمای خصوصی"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"سرپرست فناوری اطلاعات آن را مسدود کرده است"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"نمیتوان این محتوا را با برنامههای کاری همرسانی کرد"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"نمیتوان این محتوا را با برنامههای کاری باز کرد"</string> diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml index 53537e67..80f39cfa 100644 --- a/java/res/values-fi/strings.xml +++ b/java/res/values-fi/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Videota ja linkkiä jaetaan}other{# videota ja linkkiä jaetaan}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Tiedostoa ja tekstiä jaetaan}other{# tiedostoa ja tekstiä jaetaan}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Tiedostoa ja linkkiä jaetaan}other{# tiedostoa ja linkkiä jaetaan}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albumia jaetaan"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Vain kuva}other{Vain kuvat}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vain video}other{Vain videot}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Vain tiedostot}other{Vain tiedostot}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Sovellus ei ole saanut tallennuslupaa mutta voi tallentaa ääntä tämän USB-laitteen avulla."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Henkilökohtainen"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Työ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Yksityinen"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Henkilökohtainen näkymä"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Työnäkymä"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Yksityinen näkymä"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT-järjestelmänvalvojasi estämä"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tätä sisältöä ei voi jakaa työsovelluksilla"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tätä sisältöä ei voi avata työsovelluksilla"</string> diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml index 5595b6cc..d5064071 100644 --- a/java/res/values-fr-rCA/strings.xml +++ b/java/res/values-fr-rCA/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Partage d\'une vidéo avec un lien}one{Partage de # vidéo avec un lien}many{Partage de # de vidéos avec un lien}other{Partage de # vidéos avec un lien}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Partage d\'un fichier avec du texte}one{Partage de # fichier avec du texte}many{Partage de # de fichiers avec du texte}other{Partage de # fichiers avec du texte}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Partage d\'un fichier avec un lien}one{Partage de # fichier avec un lien}many{Partage de # de fichiers avec un lien}other{Partage de # fichiers avec un lien}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Album partagé"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image uniquement}one{Image uniquement}many{Images uniquement}other{Images uniquement}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vidéo uniquement}one{Vidéo uniquement}many{Vidéos uniquement}other{Vidéos uniquement}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fichier uniquement}one{Fichier uniquement}many{Fichiers uniquement}other{Fichiers uniquement}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Cette application n\'a pas été autorisée à effectuer des enregistrements, mais elle pourrait capturer du contenu audio par l\'intermédiaire de cet appareil USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personnel"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Professionnel"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privé"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Affichage personnel"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Affichage professionnel"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Affichage privé"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqué par votre administrateur informatique"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Impossible de partager ce contenu avec des applications professionnelles"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applications professionnelles"</string> diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml index 5f0c85e0..ff0c5199 100644 --- a/java/res/values-fr/strings.xml +++ b/java/res/values-fr/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Partager 1 vidéo avec un lien}one{Partager # vidéo avec un lien}many{Partager # vidéos avec un lien}other{Partager # vidéos avec un lien}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Partager 1 fichier avec du texte}one{Partager # fichier avec du texte}many{Partager # fichiers avec du texte}other{Partager # fichiers avec du texte}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Partager 1 fichier avec un lien}one{Partager # fichier avec un lien}many{Partager # fichiers avec un lien}other{Partager # fichiers avec un lien}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Partage de l\'album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image uniquement}one{Image uniquement}many{Images uniquement}other{Images uniquement}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vidéo uniquement}one{Vidéo uniquement}many{Vidéos uniquement}other{Vidéos uniquement}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fichier uniquement}one{Fichier uniquement}many{Fichiers uniquement}other{Fichiers uniquement}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Cette application n\'a pas reçu l\'autorisation d\'enregistrer des contenus audio, mais peut le faire via ce périphérique USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personnel"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Professionnel"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Mode privé"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vue personnelle"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vue professionnelle"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Affichage en mode privé"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqué par votre administrateur informatique"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Impossible de partager ce contenu avec des applis professionnelles"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applis professionnelles"</string> diff --git a/java/res/values-gl/strings.xml b/java/res/values-gl/strings.xml index 60dc78de..f4727877 100644 --- a/java/res/values-gl/strings.xml +++ b/java/res/values-gl/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartindo vídeo con ligazón}other{Compartindo # vídeos con ligazón}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartindo ficheiro con texto}other{Compartindo # ficheiros con texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartindo ficheiro con ligazón}other{Compartindo # ficheiros con ligazón}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Compartindo álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Só a imaxe}other{Só as imaxes}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Só o vídeo}other{Só os vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Só o ficheiro}other{Só os ficheiros}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta aplicación non está autorizada a realizar gravacións, pero podería capturar audio a través deste dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Persoal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Traballo"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista persoal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de traballo"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"O teu administrador de TI bloqueou a instalación"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Este contido non pode compartirse con aplicacións do traballo"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contido non pode abrirse con aplicacións do traballo"</string> diff --git a/java/res/values-gu/strings.xml b/java/res/values-gu/strings.xml index db3bd59a..22826860 100644 --- a/java/res/values-gu/strings.xml +++ b/java/res/values-gu/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{લિંક સાથે વીડિયો શેર કરી રહ્યાં છીએ}one{લિંક સાથે # વીડિયો શેર કરી રહ્યાં છીએ}other{લિંક સાથે # વીડિયો શેર કરી રહ્યાં છીએ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ટેક્સ્ટ સાથે ફાઇલ શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{લિંક સાથે ફાઇલ શેર કરી રહ્યાં છીએ}one{લિંક સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}other{લિંક સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"આલ્બમ શેર કરી રહ્યાં છીએ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{માત્ર છબી}one{માત્ર છબી}other{માત્ર છબીઓ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ફક્ત વીડિયો}one{ફક્ત વીડિયો}other{ફક્ત વીડિયો}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ફક્ત ફાઇલ}one{ફક્ત ફાઇલ}other{ફક્ત ફાઇલો}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"આ ઍપને રેકૉર્ડ કરવાની પરવાનગી આપવામાં આવી નથી પરંતુ તે આ USB ડિવાઇસ મારફતે ઑડિયો કૅપ્ચર કરી શકે છે."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"વ્યક્તિગત"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ઑફિસ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ખાનગી"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"વ્યક્તિગત વ્યૂ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ઑફિસ વ્યૂ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ખાનગી વ્યૂ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"તમારા IT વ્યવસ્થાપકે બ્લૉક કર્યું છે"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"આ કન્ટેન્ટ ઑફિસ માટેની ઍપ સાથે શેર કરી શકાતું નથી"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"આ કન્ટેન્ટ ઑફિસ માટેની ઍપ વડે ખોલી શકાતું નથી"</string> diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml index b722e0ce..933956bd 100644 --- a/java/res/values-hi/strings.xml +++ b/java/res/values-hi/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंक के साथ वीडियो शेयर किया जा रहा है}one{लिंक के साथ # वीडियो शेयर किया जा रहा है}other{लिंक के साथ # वीडियो शेयर किए जा रहे हैं}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{टेक्स्ट के साथ फ़ाइल शेयर की जा रही है}one{टेक्स्ट के साथ # फ़ाइल शेयर की जा रही है}other{टेक्स्ट के साथ # फ़ाइलें शेयर की जा रही हैं}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंक के साथ फ़ाइल शेयर की जा रही है}one{लिंक के साथ # फ़ाइल शेयर की जा रही है}other{लिंक के साथ # फ़ाइलें शेयर की जा रही हैं}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"एल्बम शेयर किया जा रहा है"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{सिर्फ़ इमेज}one{सिर्फ़ इमेज}other{सिर्फ़ इमेज}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{सिर्फ़ वीडियो}one{सिर्फ़ वीडियो}other{सिर्फ़ वीडियो}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{सिर्फ़ फ़ाइल}one{सिर्फ़ फ़ाइल}other{सिर्फ़ फ़ाइलें}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"इस ऐप्लिकेशन को रिकॉर्ड करने की अनुमति नहीं दी गई है. हालांकि, ऐप्लिकेशन इस यूएसबी डिवाइस से ऐसा कर सकता है."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"निजी प्रोफ़ाइल"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"वर्क प्रोफ़ाइल"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"निजी"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"निजी व्यू"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"वर्क व्यू"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"निजी व्यू"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"आपके आईटी एडमिन ने इस कॉन्टेंट को शेयर करने की सुविधा ब्लॉक कर रखी है"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"इस कॉन्टेंट को ऑफ़िस के काम से जुड़े ऐप्लिकेशन का इस्तेमाल करके, शेयर नहीं किया जा सकता"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"इस कॉन्टेंट को ऑफ़िस के काम से जुड़े ऐप्लिकेशन पर खोला नहीं जा सकता"</string> diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml index e2d71b37..853deacb 100644 --- a/java/res/values-hr/strings.xml +++ b/java/res/values-hr/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{i još # datoteka}one{i još # datoteka}few{i još # datoteke}other{i još # datoteka}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Dijeli se tekst"</string>      <string name="sharing_link" msgid="2307694372813942916">"Dijeli se veza"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Dijeli se slika}one{Dijeli se # slika}few{Dijele se # slike}other{Dijeli se # slika}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Podijelite sliku}one{Podijelite # sliku}few{Podijelite # slike}other{Podijelite # slika}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Dijeli se videozapis}one{Dijeli se # videozapis}few{Dijele se # videozapisa}other{Dijeli se # videozapisa}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Dijeli se # datoteka}one{Dijeli se # datoteka}few{Dijele se # datoteke}other{Dijeli se # datoteka}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Dijeli se slika s tekstom}one{Dijeli se # slika s tekstom}few{Dijele se # slike s tekstom}other{Dijeli se # slika s tekstom}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Dijeli se videozapis s vezom}one{Dijeli se # videozapis s vezom}few{Dijele se # videozapisa s vezom}other{Dijeli se # videozapisa s vezom}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Dijeli se datoteka s tekstom}one{Dijeli se # datoteka s tekstom}few{Dijele se # datoteke s tekstom}other{Dijeli se # datoteka s tekstom}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Dijeli se datoteka s vezom}one{Dijeli se # datoteka s vezom}few{Dijele se # datoteke s vezom}other{Dijeli se # datoteka s vezom}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Dijeljenje albuma"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videozapis}one{Samo videozapisi}few{Samo videozapisi}other{Samo videozapisi}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo datoteka}one{Samo datoteke}few{Samo datoteke}other{Samo datoteke}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacija nema dopuštenje za snimanje, no mogla bi primati zvuk putem ovog USB uređaja."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Osobno"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Posao"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobni prikaz"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Poslovni prikaz"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatni prikaz"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokirao vaš IT administrator"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Taj se sadržaj ne može dijeliti pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Taj se sadržaj ne može otvoriti pomoću poslovnih aplikacija"</string> diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml index 53ddba7f..fa4f23d2 100644 --- a/java/res/values-hu/strings.xml +++ b/java/res/values-hu/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Videó megosztása linkkel}other{# videó megosztása linkkel}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Fájl megosztása szöveggel}other{# fájl megosztása szöveggel}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Fájl megosztása linkkel}other{# fájl megosztása linkkel}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Album megosztása"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Csak kép}other{Csak képek}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Csak videó}other{Csak videók}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Csak fájl}other{Csak fájlok}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ez az alkalmazás nem rendelkezik rögzítési engedéllyel, de ezzel az USB-eszközzel képes a hangfelvételre."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Személyes"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Munka"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privát"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Személyes nézet"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Munkanézet"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privát nézet"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Rendszergazda által letiltva"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ez a tartalom nem osztható meg munkahelyi alkalmazásokkal"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ez a tartalom nem nyitható meg munkahelyi alkalmazásokkal"</string> diff --git a/java/res/values-hy/strings.xml b/java/res/values-hy/strings.xml index 6a83cdaa..ec8d1355 100644 --- a/java/res/values-hy/strings.xml +++ b/java/res/values-hy/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Տեսանյութի ուղարկում հղման միջոցով}one{# տեսանյութի ուղարկում հղման միջոցով}other{# տեսանյութի ուղարկում հղման միջոցով}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}one{# ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}other{# ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Ֆայլի ուղարկում հղման միջոցով}one{# ֆայլի ուղարկում հղման միջոցով}other{# ֆայլի ուղարկում հղման միջոցով}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Ալբոմը դարձվում է ընդհանուր"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Միայն պատկերը}one{Միայն պատկերը}other{Միայն պատկերները}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Միայն տեսանյութը}one{Միայն տեսանյութը}other{Միայն տեսանյութերը}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Միայն ֆայլը}one{Միայն ֆայլը}other{Միայն ֆայլերը}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Հավելվածը ձայնագրելու թույլտվություն չունի, սակայն կկարողանա գրանցել ձայնն այս USB սարքի միջոցով։"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Անձնական"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Աշխատանքային"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Անձնական"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Անձնական"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Աշխատանքային"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Անձնական դիտակերպ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Արգելափակվել է ձեր ՏՏ ադմինիստրատորի կողմից"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Այս բովանդակությունը հնարավոր չէ ուղարկել աշխատանքային հավելվածներով"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Այս բովանդակությունը հնարավոր չէ բացել աշխատանքային հավելվածներով"</string> diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml index d7400b80..83d18123 100644 --- a/java/res/values-in/strings.xml +++ b/java/res/values-in/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Membagikan video dengan link}other{Membagikan # video dengan link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Membagikan file dengan teks}other{Membagikan # file dengan teks}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Membagikan file dengan link}other{Membagikan # file dengan link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Berbagi album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Khusus gambar}other{Khusus gambar}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Khusus video}other{Khusus video}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Khusus file}other{Khusus file}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aplikasi ini tidak diberi izin merekam, tetapi dapat merekam audio melalui perangkat USB ini."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Pribadi"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Kerja"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Pribadi"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Tampilan pribadi"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Tampilan kerja"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Tampilan pribadi"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Diblokir oleh admin IT Anda"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Konten ini tidak dapat dibagikan dengan aplikasi kerja"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Konten ini tidak dapat dibuka dengan aplikasi kerja"</string> diff --git a/java/res/values-is/strings.xml b/java/res/values-is/strings.xml index 8e0a9f4f..a07fc1c8 100644 --- a/java/res/values-is/strings.xml +++ b/java/res/values-is/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deilir myndskeiði með tengli}one{Deilir # myndskeiði með tengli}other{Deilir # myndskeiðum með tengli}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deilir skrá með texta}one{Deilir # skrá með texta}other{Deilir # skrám með texta}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deilir skrá með tengli}one{Deilir # skrá með tengli}other{Deilir # skrám með tengli}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deilir albúmi"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Eingöngu mynd}one{Eingöngu myndir}other{Eingöngu myndir}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Eingöngu myndskeið}one{Eingöngu myndskeið}other{Eingöngu myndskeið}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Eingöngu skrá}one{Eingöngu skrár}other{Eingöngu skrár}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Þetta forrit hefur ekki fengið heimild fyrir upptöku en gæti tekið upp hljóð í gegnum þetta USB-tæki."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Persónulegt"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Vinna"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Lokað"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persónulegt yfirlit"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vinnuyfirlit"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Lokuð stilling"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Útilokað af kerfisstjóra"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ekki er hægt að deila þessu efni með vinnuforritum"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ekki er hægt að opna þetta efni með vinnuforritum"</string> diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml index 38aba0c2..a68fe2bb 100644 --- a/java/res/values-it/strings.xml +++ b/java/res/values-it/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Condivisione video con link in corso…}many{Condivisione # video con link in corso…}other{Condivisione # video con link in corso…}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Condivisione file con messaggio in corso…}many{Condivisione # file con messaggio in corso…}other{Condivisione # file con messaggio in corso…}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Condivisione file con link in corso…}many{Condivisione # file con link in corso…}other{Condivisione # file con link in corso…}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Condivisione album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Soltanto l\'immagine}many{Soltanto le immagini}other{Soltanto le immagini}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Soltanto il video}many{Soltanto i video}other{Soltanto i video}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Soltanto il file}many{Soltanto i file}other{Soltanto i file}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"A questa app non è stata concessa l\'autorizzazione di registrazione, ma l\'app potrebbe acquisire l\'audio tramite questo dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personale"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Lavoro"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privata"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualizzazione personale"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualizzazione di lavoro"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualizzazione privata"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloccati dall\'amministratore IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Questi contenuti non possono essere condivisi con app di lavoro"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Questi contenuti non possono essere aperti con app di lavoro"</string> diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml index c79425d8..30ecfe02 100644 --- a/java/res/values-iw/strings.xml +++ b/java/res/values-iw/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{שיתוף סרטון עם קישור}one{שיתוף # סרטונים עם קישור}two{שיתוף # סרטונים עם קישור}other{שיתוף # סרטונים עם קישור}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{שיתוף קובץ עם טקסט}one{שיתוף # קבצים עם טקסט}two{שיתוף # קבצים עם טקסט}other{שיתוף # קבצים עם טקסט}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{שיתוף תמונה עם קישור}one{שיתוף # תמונות עם קישור}two{שיתוף # תמונות עם קישור}other{שיתוף # תמונות עם קישור}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"שיתוף האלבום"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{תמונה בלבד}one{תמונות בלבד}two{תמונות בלבד}other{תמונות בלבד}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{סרטון בלבד}one{סרטונים בלבד}two{סרטונים בלבד}other{סרטונים בלבד}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{קובץ בלבד}one{קבצים בלבד}two{קבצים בלבד}other{קבצים בלבד}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"לאפליקציה זו לא ניתנה הרשאת הקלטה, אבל אפשר להקליט אודיו באמצעות התקן ה-USB הזה."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"אישי"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"עבודה"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"פרטי"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"תצוגה אישית"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"תצוגת עבודה"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"תצוגה פרטית"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"נחסם על ידי מנהל ה-IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"אי אפשר לשתף את התוכן הזה עם אפליקציות לעבודה"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"אי אפשר לפתוח את התוכן הזה באמצעות אפליקציות לעבודה"</string> diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml index 15c2277b..a9f79a48 100644 --- a/java/res/values-ja/strings.xml +++ b/java/res/values-ja/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{リンク付き動画を共有中}other{リンク付き動画を # 件共有中}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{テキスト付きファイルを共有中}other{テキスト付きファイルを # 件共有中}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{リンク付きファイルを共有中}other{リンク付きファイルを # 件共有中}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"アルバムの共有"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{画像のみ}other{画像のみ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{動画のみ}other{動画のみ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ファイルのみ}other{ファイルのみ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"このアプリに録音権限は付与されていませんが、この USB デバイスから音声を収集できるようになります。"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"個人用"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"仕事用"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"プライベート"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人用ビュー"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"仕事用ビュー"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"プライベート ビュー"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT 管理者によりブロックされました"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"このコンテンツを仕事用アプリと共有することはできません"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"このコンテンツを仕事用アプリで開くことはできません"</string> diff --git a/java/res/values-ka/strings.xml b/java/res/values-ka/strings.xml index 88bc15ac..86991fab 100644 --- a/java/res/values-ka/strings.xml +++ b/java/res/values-ka/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ვიდეო ზიარდება ბმულით}other{# ვიდეო ზიარდება ბმულით}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ფაილი ზიარდება ტექსტით}other{# ფაილი ზიარდება ტექსტით}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ფაილი ზიარდება ბმულით}other{# ფაილი ზიარდება ბმულით}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"გაზიარებული ალბომი"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{მხოლოდ სურათი}other{მხოლოდ სურათები}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{მხოლოდ ვიდეო}other{მხოლოდ ვიდეოები}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{მხოლოდ ფაილი}other{მხოლოდ ფაილები}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ამ აპს არ აქვს მინიჭებული ჩაწერის ნებართვა, მაგრამ შეუძლია ჩაიწეროს აუდიო ამ USB მოწყობილობის მეშვეობით."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"პირადი"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"სამსახური"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"კერძო"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"პირადი ხედი"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"სამსახურის ხედი"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"პირადი სივრცე"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"დაბლოკილია თქვენი IT-ადმინისტრატორის მიერ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ამ კონტენტის სამსახურის აპებისთვის გაზიარება შეუძლებელია"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ამ კონტენტის სამსახურის აპებით გახსნა შეუძლებელია"</string> diff --git a/java/res/values-kk/strings.xml b/java/res/values-kk/strings.xml index 7b195799..2ac5fa0e 100644 --- a/java/res/values-kk/strings.xml +++ b/java/res/values-kk/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Сілтемесі бар бейне жіберу}other{Сілтемесі бар # бейне жіберу}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Мәтіні бар файл жіберу}other{Мәтіні бар # файл жіберу}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Сілтемесі бар файл жіберу}other{Сілтемесі бар # файл жіберу}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Альбомды бөлісу"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Тек сурет}other{Тек суреттер}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Тек бейне}other{Тек бейнелер}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Тек файл}other{Тек файлдар}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Қолданбаға жазу рұқсаты берілмеді, бірақ ол осы USB құрылғысы арқылы дыбыс жаза алады."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Жеке"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Жұмыс"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Құпия"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көру"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жұмыс деректерін көру"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Құпия көрініс"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Әкімшіңіз бөгеген"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Бұл контентті жұмыс қолданбаларымен бөлісу мүмкін емес."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бұл контентті жұмыс қолданбаларымен ашу мүмкін емес."</string> diff --git a/java/res/values-km/strings.xml b/java/res/values-km/strings.xml index ae956af3..f0f25e41 100644 --- a/java/res/values-km/strings.xml +++ b/java/res/values-km/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ចែករំលែកវីដេអូជាមួយតំណ}other{ចែករំលែក # វីដេអូជាមួយតំណ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ចែករំលែកឯកសារជាមួយអក្សរ}other{ចែករំលែក # ឯកសារជាមួយអក្សរ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ចែករំលែកឯកសារជាមួយតំណ}other{ចែករំលែកឯកសារ # ជាមួយតំណ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"កំពុងចែករំលែកអាល់ប៊ុម"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{រូបភាពតែប៉ុណ្ណោះ}other{រូបភាពតែប៉ុណ្ណោះ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{វីដេអូតែប៉ុណ្ណោះ}other{វីដេអូតែប៉ុណ្ណោះ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ឯកសារតែប៉ុណ្ណោះ}other{ឯកសារតែប៉ុណ្ណោះ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"កម្មវិធីនេះមិនទាន់បានទទួលសិទ្ធិថតសំឡេងនៅឡើយទេ ប៉ុន្តែអាចថតសំឡេងតាមរយៈឧបករណ៍ USB នេះបាន។"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ផ្ទាល់ខ្លួន"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ការងារ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ឯកជន"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ទិដ្ឋភាពផ្ទាល់ខ្លួន"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ទិដ្ឋភាពការងារ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ទិដ្ឋភាពឯកជន"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"បានទប់ស្កាត់ដោយអ្នកគ្រប់គ្រងផ្នែកព័ត៌មានវិទ្យារបស់អ្នក"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ខ្លឹមសារនេះមិនអាចចែករំលែកតាមរយៈកម្មវិធីការងារបានទេ"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ខ្លឹមសារនេះមិនអាចបើកតាមរយៈកម្មវិធីការងារបានទេ"</string> diff --git a/java/res/values-kn/strings.xml b/java/res/values-kn/strings.xml index 505277c6..101a1bc0 100644 --- a/java/res/values-kn/strings.xml +++ b/java/res/values-kn/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ಲಿಂಕ್ನೊಂದಿಗೆ ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್ನೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್ನೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ಲಿಂಕ್ನೊಂದಿಗೆ ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್ನೊಂದಿಗೆ # ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್ನೊಂದಿಗೆ # ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ಆಲ್ಬಮ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ಚಿತ್ರ ಮಾತ್ರ}one{ಚಿತ್ರಗಳು ಮಾತ್ರ}other{ಚಿತ್ರಗಳು ಮಾತ್ರ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ವೀಡಿಯೊ ಮಾತ್ರ}one{ವೀಡಿಯೊಗಳು ಮಾತ್ರ}other{ವೀಡಿಯೊಗಳು ಮಾತ್ರ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ಫೈಲ್ ಮಾತ್ರ}one{ಫೈಲ್ಗಳು ಮಾತ್ರ}other{ಫೈಲ್ಗಳು ಮಾತ್ರ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ಈ ಆ್ಯಪ್ಗೆ ರೆಕಾರ್ಡ್ ಅನುಮತಿಯನ್ನು ನೀಡಲಾಗಿಲ್ಲ, ಆದರೆ ಈ USB ಸಾಧನದ ಮೂಲಕ ಆಡಿಯೊವನ್ನು ಸೆರೆಹಿಡಿಯಬಲ್ಲದು."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ವೈಯಕ್ತಿಕ"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ಕೆಲಸ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ಖಾಸಗಿ"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ವೈಯಕ್ತಿಕ ವೀಕ್ಷಣೆ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ಕೆಲಸದ ವೀಕ್ಷಣೆ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ಖಾಸಗಿ ವೀಕ್ಷಣೆ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ನಿಮ್ಮ IT ನಿರ್ವಾಹಕರಿಂದ ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ಗಳ ಈ ವಿಷಯವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುವುದಿಲ್ಲ"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ಗಳ ಈ ವಿಷಯವನ್ನು ತೆರೆಯಲಾಗುವುದಿಲ್ಲ"</string> diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml index e9e908be..1b4f2264 100644 --- a/java/res/values-ko/strings.xml +++ b/java/res/values-ko/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{링크로 동영상 공유 중}other{링크로 동영상 #개 공유 중}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{텍스트로 파일 공유 중}other{텍스트로 파일 #개 공유 중}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{링크로 파일 공유 중}other{링크로 파일 #개 공유 중}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"앨범 공유"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{이미지만}other{이미지만}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{동영상만}other{동영상만}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{파일만}other{파일만}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"이 앱에는 녹음 권한이 부여되지 않았지만, 이 USB 기기를 통해 오디오를 녹음할 수 있습니다."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"개인"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"직장"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"비공개"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"개인 뷰"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"직장 뷰"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"비공개 뷰"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT 관리자에 의해 차단됨"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"이 콘텐츠는 직장 앱을 통해 공유할 수 없습니다."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"이 콘텐츠는 직장 앱으로 열 수 없습니다."</string> diff --git a/java/res/values-ky/strings.xml b/java/res/values-ky/strings.xml index 311a2169..33c58be4 100644 --- a/java/res/values-ky/strings.xml +++ b/java/res/values-ky/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Видеону шилтеме менен жөнөтүү}other{# видеону шилтеме менен жөнөтүү}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Файлды текст менен жөнөтүү}other{# файлды текст менен жөнөтүү}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Файлды шилтеме менен жөнөтүү}other{# файлды шилтеме менен жөнөтүү}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Альбом бөлүшүлүүдө"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Сүрөт гана}other{Сүрөттөр гана}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Видео гана}other{Видеолор гана}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Файл гана}other{Файлдар гана}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Бул колдонмого жаздырууга уруксат берилген эмес, бирок ушул USB түзмөгү аркылуу үндөрдү жаза алат."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Жеке"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Жумуш"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Купуя"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көрүнүш"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жумуш көрүнүшү"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Купуя көрүнүш"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT администраторуңуз бөгөттөп койгон"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Бул нерсени жумуш колдонмолору менен бөлүшө албайсыз"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бул нерсени жумуш колдонмолору менен ача албайсыз"</string> diff --git a/java/res/values-lo/strings.xml b/java/res/values-lo/strings.xml index 48e9a074..7a78f9a3 100644 --- a/java/res/values-lo/strings.xml +++ b/java/res/values-lo/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອພ້ອມລິ້ງ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ກຳລັງແບ່ງປັນໄຟລ໌ພ້ອມຂໍ້ຄວາມ}other{ກຳລັງແບ່ງປັນ # ໄຟລ໌ພ້ອມຂໍ້ຄວາມ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ກຳລັງແບ່ງປັນໄຟລ໌ພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ໄຟລ໌ພ້ອມລິ້ງ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ກຳລັງແບ່ງປັນອະລະບ້ຳ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ຮູບເທົ່ານັ້ນ}other{ຮູບເທົ່ານັ້ນ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ວິດີໂອເທົ່ານັ້ນ}other{ວິດີໂອເທົ່ານັ້ນ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ໄຟລ໌ເທົ່ານັ້ນ}other{ໄຟລ໌ເທົ່ານັ້ນ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ແອັບນີ້ບໍ່ໄດ້ຮັບສິດອະນຸຍາດໃນການບັນທຶກ ແຕ່ສາມາດບັນທຶກສຽງໄດ້ຜ່ານອຸປະກອນ USB ນີ້."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ສ່ວນຕົວ"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ວຽກ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ສ່ວນຕົວ"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ມຸມມອງສ່ວນຕົວ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ມຸມມອງວຽກ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ມຸມມອງສ່ວນຕົວ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ຖືກບລັອກໄວ້ໂດຍຜູ້ເບິ່ງແຍງໄອທີຂອງທ່ານ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກແບ່ງປັນກັບແອັບບ່ອນເຮັດວຽກໄດ້"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກເປີດໄດ້ດ້ວຍແອັບບ່ອນເຮັດວຽກ"</string> diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml index 51ffbbff..a031b1ae 100644 --- a/java/res/values-lt/strings.xml +++ b/java/res/values-lt/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bendrinamas vaizdo įrašas su nuoroda}one{Bendrinamas # vaizdo įrašas su nuoroda}few{Bendrinami # vaizdo įrašai su nuoroda}many{Bendrinamas # vaizdo įrašo su nuoroda}other{Bendrinama # vaizdo įrašų su nuoroda}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Bendrinamas failas su tekstu}one{Bendrinamas # failas su tekstu}few{Bendrinami # failai su tekstu}many{Bendrinama # failo su tekstu}other{Bendrinama # failų su tekstu}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bendrinamas failas su nuoroda}one{Bendrinamas # failas su nuoroda}few{Bendrinami # failai su nuoroda}many{Bendrinama # failo su nuoroda}other{Bendrinama # failų su nuoroda}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Bendrinamas albumas"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tik vaizdas}one{Tik vaizdai}few{Tik vaizdai}many{Tik vaizdai}other{Tik vaizdai}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tik vaizdo įrašas}one{Tik vaizdo įrašai}few{Tik vaizdo įrašai}many{Tik vaizdo įrašai}other{Tik vaizdo įrašai}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tik failas}one{Tik failai}few{Tik failai}many{Tik failai}other{Tik failai}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Šiai programai nebuvo suteiktas leidimas įrašyti, bet ji gali užfiksuoti garsą per šį USB įrenginį."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Asmeninis"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Darbo"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privatus"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Asmeninė peržiūra"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Darbo peržiūra"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatus rodinys"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Užblokavo jūsų IT administratorius"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Šio turinio negalima bendrinti su darbo programomis"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šio turinio negalima atidaryti naudojant darbo programas"</string> diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml index de5c352b..ead503a4 100644 --- a/java/res/values-lv/strings.xml +++ b/java/res/values-lv/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Tiek kopīgots videoklips ar saiti}zero{Tiek kopīgoti # videoklipi ar saitēm}one{Tiek kopīgots # videoklips ar saitēm}other{Tiek kopīgoti # videoklipi ar saitēm}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Tiek kopīgots fails ar tekstu}zero{Tiek kopīgoti # faili ar tekstu}one{Tiek kopīgots # fails ar tekstu}other{Tiek kopīgoti # faili ar tekstu}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Tiek kopīgots fails ar saiti}zero{Tiek kopīgoti # faili ar saitēm}one{Tiek kopīgots # fails ar saitēm}other{Tiek kopīgoti # faili ar saitēm}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Notiek albuma kopīgošana"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tikai attēls}zero{Tikai attēli}one{Tikai attēli}other{Tikai attēli}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tikai videoklips}zero{Tikai videoklipi}one{Tikai videoklipi}other{Tikai videoklipi}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tikai fails}zero{Tikai faili}one{Tikai faili}other{Tikai faili}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Šai lietotnei nav piešķirta ierakstīšanas atļauja, taču tā varētu tvert audio, izmantojot šo USB ierīci."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Privātais profils"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Darba profils"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privāts"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personisks skats"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Darba skats"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privātais skats"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloķējis jūsu IT administrators"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Šo saturu nevar kopīgot ar darba lietotnēm"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šo saturu nevar atvērt darba lietotnēs"</string> diff --git a/java/res/values-mk/strings.xml b/java/res/values-mk/strings.xml index 7ef3a9ca..b1e73dea 100644 --- a/java/res/values-mk/strings.xml +++ b/java/res/values-mk/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Се споделува видео со линк}one{Се споделуваат # видео со линк}other{Се споделуваat # видеa со линк}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Се споделува датотека со SMS}one{Се споделуваат # датотека со SMS}other{Се споделуваат # датотеки со SMS}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Се споделува датотека со линк}one{Се споделуваат # датотека со линк}other{Се споделуваат # датотеки со линк}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Споделување албум"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само слика}one{Само слики}other{Само слики}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видео}one{Само видеа}other{Само видеа}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само датотека}one{Само датотеки}other{Само датотеки}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"На апликацијава не ѝ е доделена дозвола за снимање, но може да снима аудио преку овој USB-уред."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Лични"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"За работа"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Приватно"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Личен приказ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Работен приказ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватен приказ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Блокирано од IT-администраторот"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Овие содржини не може да се споделуваат со работни апликации"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овие содржини не може да се отвораат со работни апликации"</string> diff --git a/java/res/values-ml/strings.xml b/java/res/values-ml/strings.xml index 03b01db9..4e80ca86 100644 --- a/java/res/values-ml/strings.xml +++ b/java/res/values-ml/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ലിങ്കിനൊപ്പം വീഡിയോ പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # വീഡിയോകൾ പങ്കിടുന്നു}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ടെക്സ്റ്റിനൊപ്പം ഫയൽ പങ്കിടുന്നു}other{ടെക്സ്റ്റിനൊപ്പം # ഫയലുകൾ പങ്കിടുന്നു}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ലിങ്കിനൊപ്പം ഫയൽ പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # ഫയലുകൾ പങ്കിടുന്നു}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ആൽബം പങ്കിടൽ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ചിത്രം മാത്രം}other{ചിത്രങ്ങൾ മാത്രം}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{വീഡിയോ മാത്രം}other{വീഡിയോകൾ മാത്രം}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ഫയൽ മാത്രം}other{ഫയലുകൾ മാത്രം}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ഈ ആപ്പിന് റെക്കോർഡ് അനുമതി നൽകിയിട്ടില്ല, എന്നാൽ ഈ USB ഉപകരണത്തിലൂടെ ഓഡിയോ ക്യാപ്ചർ ചെയ്യാനാവും."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"വ്യക്തിപരം"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ഔദ്യോഗികം"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"സ്വകാര്യം"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"വ്യക്തിപര കാഴ്ച"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ഔദ്യോഗിക കാഴ്ച"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"സ്വകാര്യ കാഴ്ച"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"നിങ്ങളുടെ ഐടി അഡ്മിൻ ബ്ലോക്ക് ചെയ്തു"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ഔദ്യോഗിക ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം പങ്കിടാനാകില്ല"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ഔദ്യോഗിക ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം തുറക്കാനാകില്ല"</string> diff --git a/java/res/values-mn/strings.xml b/java/res/values-mn/strings.xml index 339ca5e4..77ef0edc 100644 --- a/java/res/values-mn/strings.xml +++ b/java/res/values-mn/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Холбоостой видео хуваалцаж байна}other{Холбоостой # видео хуваалцаж байна}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Тексттэй файл хуваалцаж байна}other{Тексттэй # файл хуваалцаж байна}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Холбоостой файл хуваалцаж байна}other{Холбоостой # файл хуваалцаж байна}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Цомог хуваалцаж байна"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Зөвхөн зураг}other{Зөвхөн зургууд}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Зөвхөн видео}other{Зөвхөн видеонууд}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Зөвхөн файл}other{Зөвхөн файлууд}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Энэ апликейшнд бичих зөвшөөрөл олгогдоогүй ч энэ USB төхөөрөмжөөр дамжуулан аудио бичиж чадсан."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Хувийн"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Ажил"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Хувийн"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Хувийн харагдах байдал"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ажлын харагдах байдал"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Хувийн харагдах байдал"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Таны IT админ блоклосон"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Энэ контентыг ажлын аппуудаар хуваалцах боломжгүй"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Энэ контентыг ажлын аппуудаар нээх боломжгүй"</string> diff --git a/java/res/values-mr/strings.xml b/java/res/values-mr/strings.xml index 5202a3b7..93db4e04 100644 --- a/java/res/values-mr/strings.xml +++ b/java/res/values-mr/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंकसह व्हिडिओ शेअर करत आहे}other{लिंकसह # व्हिडिओ शेअर करत आहे}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{मजकुरासह फाइल शेअर करत आहे}other{मजकुरासह # फाइल शेअर करत आहे}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंकसह फाइल शेअर करत आहे}other{लिंकसह # फाइल शेअर करत आहे}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"अल्बम शेअर करत आहे"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{फक्त इमेज}other{फक्त इमेज}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{फक्त व्हिडिओ}other{फक्त व्हिडिओ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{फक्त फाइल}other{फक्त फाइल}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"या अॅपला रेकॉर्ड करण्याची परवानगी दिली गेली नाही पण हे USB डिव्हाइस वापरून ऑडिओ कॅप्चर केला जाऊ शकतो."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"वैयक्तिक"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"कार्य"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"खाजगी"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"वैयक्तिक दृश्य"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"कार्य दृश्य"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"खाजगी व्ह्यू"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"तुमच्या IT ॲडमिनने ब्लॉक केले"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"हा आशय कार्य ॲप्ससह शेअर केला जाऊ शकत नाही"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"हा आशय कार्य ॲप्स वापरून उघडला जाऊ शकत नाही"</string> diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml index f1ac4d1d..968c4090 100644 --- a/java/res/values-ms/strings.xml +++ b/java/res/values-ms/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Berkongsi video dengan pautan}other{Berkongsi # video dengan pautan}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Berkongsi fail dengan teks}other{Berkongsi # fail dengan teks}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Berkongsi fail dengan pautan}other{Berkongsi # fail dengan pautan}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Berkongsi album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Imej sahaja}other{Imej sahaja}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video sahaja}other{Video sahaja}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fail sahaja}other{Fail sahaja}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Apl ini belum diberikan kebenaran merakam tetapi dapat merakam audio melalui peranti USB ini."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Peribadi"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Kerja"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Peribadi"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Paparan peribadi"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Paparan kerja"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Paparan peribadi"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Disekat oleh pentadbir IT anda"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Kandungan ini tidak boleh dikongsi dengan apl kerja"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kandungan ini tidak boleh dibuka dengan apl kerja"</string> diff --git a/java/res/values-my/strings.xml b/java/res/values-my/strings.xml index c3ab1ee2..d31d8a48 100644 --- a/java/res/values-my/strings.xml +++ b/java/res/values-my/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{လင့်ခ်ပါသောဗီဒီယိုကို မျှဝေနေသည်}other{လင့်ခ်ပါသောဗီဒီယို # ခုကို မျှဝေနေသည်}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{စာသားပါသောဖိုင်ကို မျှဝေနေသည်}other{စာသားပါသောဖိုင် # ခုကို မျှဝေနေသည်}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{လင့်ခ်ပါသောဖိုင်ကို မျှဝေနေသည်}other{လင့်ခ်ပါသောဖိုင် # ခုကို မျှဝေနေသည်}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"အယ်လ်ဘမ် မျှဝေနေသည်"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ပုံသာလျှင်}other{ပုံများသာလျှင်}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ဗီဒီယိုသာလျှင်}other{ဗီဒီယိုများသာလျှင်}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ဖိုင်သာလျှင်}other{ဖိုင်များသာလျှင်}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ဤအက်ပ်ကို အသံဖမ်းခွင့် ပေးမထားသော်လည်း ၎င်းသည် ဤ USB စက်ပစ္စည်းမှတစ်ဆင့် အသံများကို ဖမ်းယူနိုင်ပါသည်။"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ကိုယ်ပိုင်"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"အလုပ်"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"သီးသန့်"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ပုဂ္ဂိုလ်ရေးဆိုင်ရာ မြင်ကွင်း"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"အလုပ် မြင်ကွင်း"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"သီးသန့်ပြသခြင်း"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"သင်၏ IT စီမံခန့်ခွဲသူက ပိတ်ထားသည်"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ဤအကြောင်းအရာကို အလုပ်သုံးအက်ပ်များဖြင့် မမျှဝေနိုင်ပါ"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ဤအကြောင်းအရာကို အလုပ်သုံးအက်ပ်များဖြင့် မဖွင့်နိုင်ပါ"</string> diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml index a2c6da68..587233b4 100644 --- a/java/res/values-nb/strings.xml +++ b/java/res/values-nb/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deler videoen med link}other{Deler # videoer med link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deler filen med tekst}other{Deler # filer med tekst}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deler filen med link}other{Deler # filer med link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deler album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Bare bildet}other{Bare bildene}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Bare videoen}other{Bare videoene}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Bare filen}other{Bare filene}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Denne appen har ikke fått tillatelse til å spille inn, men kan ta opp lyd med denne USB-enheten."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personlig"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Jobb"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personlig visning"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Jobbvisning"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privat visning"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokkert av IT-administratoren din"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Dette innholdet kan ikke deles med jobbapper"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette innholdet kan ikke åpnes med jobbapper"</string> diff --git a/java/res/values-ne/strings.xml b/java/res/values-ne/strings.xml index 176067f2..a800d0b0 100644 --- a/java/res/values-ne/strings.xml +++ b/java/res/values-ne/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंक भएको भिडियो सेयर गरिँदै छ}other{लिंक भएका # वटा भिडियो सेयर गरिँदै छन्}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{टेक्स्ट भएको फाइल सेयर गरिँदै छ}other{टेक्स्ट भएका # वटा फाइल सेयर गरिँदै छन्}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंक भएको फाइल सेयर गरिँदै छ}other{लिंक भएका # वटा फाइल सेयर गरिँदै छन्}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"एल्बम सेयर गरिँदै छ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{फोटो मात्र}other{फोटोहरू मात्र}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{भिडियो मात्र}other{भिडियोहरू मात्र}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{फाइल मात्र}other{फाइलहरू मात्र}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"यो एपलाई रेकर्ड गर्ने अनुमति प्रदान गरिएको छैन तर यसले यो USB यन्त्रमार्फत अडियो क्याप्चर गर्न सक्छ।"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"व्यक्तिगत"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"काम"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"निजी"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"व्यक्तिगत दृश्य"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"कार्य दृश्य"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"निजी भ्यू"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"तपाईंका IT एड्मिनले ब्लक गर्नुभएको छ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"यो सामग्री कामसम्बन्धी एपहरूमार्फत सेयर गर्न मिल्दैन"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"यो सामग्री कामसम्बन्धी एपहरूमार्फत खोल्न मिल्दैन"</string> diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml index 7ef1513b..4f8c48b2 100644 --- a/java/res/values-nl/strings.xml +++ b/java/res/values-nl/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Video delen via link}other{# video\'s delen via link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Bestand delen via tekstbericht}other{# bestanden delen via tekstbericht}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bestand delen via link}other{# bestanden delen via link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Album wordt gedeeld"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Alleen afbeelding}other{Alleen afbeeldingen}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Alleen video}other{Alleen video\'s}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Alleen bestand}other{Alleen bestanden}}"</string> @@ -76,14 +77,16 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Deze app heeft geen opnamerechten gekregen, maar zou audio kunnen vastleggen via dit USB-apparaat."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Persoonlijk"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Werk"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privé"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persoonlijke weergave"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Werkweergave"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privéweergave"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Geblokkeerd door je IT-beheerder"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Deze content kan niet worden gedeeld met werk-apps"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Deze content kan niet worden geopend met werk-apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Deze content kan niet worden gedeeld met persoonlijke apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Deze content kan niet worden geopend met persoonlijke apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werk-apps zijn onderbroken"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werk-apps zijn gepauzeerd"</string>      <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hervatten"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Geen werk-apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Geen persoonlijke apps"</string> diff --git a/java/res/values-or/strings.xml b/java/res/values-or/strings.xml index 93c60db2..b41e4cd2 100644 --- a/java/res/values-or/strings.xml +++ b/java/res/values-or/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ଲିଙ୍କ ସହ ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ଟେକ୍ସଟ ସହ ଫାଇଲ ସେୟାର କରାଯାଉଛି}other{ଟେକ୍ସଟ ସହ #ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ଲିଙ୍କ ସହ ଫାଇଲ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ଆଲବମ ସେୟାର କରାଯାଉଛି"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{କେବଳ ଇମେଜ}other{କେବଳ ଇମେଜଗୁଡ଼ିକ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{କେବଳ ଭିଡିଓ}other{କେବଳ ଭିଡିଓଗୁଡ଼ିକ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{କେବଳ ଫାଇଲ}other{କେବଳ ଫାଇଲଗୁଡ଼ିକ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ଏହି ଆପ୍କୁ ରେକର୍ଡ କରିବାକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ କିନ୍ତୁ ଏହି USB ଡିଭାଇସ୍ ଜରିଆରେ ଅଡିଓ କ୍ୟାପ୍ଚର୍ କରିପାରିବ।"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ବ୍ୟକ୍ତିଗତ"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ୱାର୍କ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ପ୍ରାଇଭେଟ"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ବ୍ୟକ୍ତିଗତ ଭ୍ୟୁ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ୱାର୍କ ଭ୍ୟୁ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ପ୍ରାଇଭେଟ ଭ୍ୟୁ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ଆପଣଙ୍କ IT ଆଡମିନଙ୍କ ଦ୍ୱାରା ବ୍ଲକ୍ କରାଯାଇଛି"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ଏହି ବିଷୟବସ୍ତୁ ୱାର୍କ ଆପଗୁଡ଼ିକରେ ସେୟାର୍ କରାଯାଇପାରିବ ନାହିଁ"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ଏହି ବିଷୟବସ୍ତୁ ୱାର୍କ ଆପଗୁଡ଼ିକରେ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string> diff --git a/java/res/values-pa/strings.xml b/java/res/values-pa/strings.xml index 872168d6..df920108 100644 --- a/java/res/values-pa/strings.xml +++ b/java/res/values-pa/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਫ਼ਾਈਲਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਫ਼ਾਈਲਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ਐਲਬਮ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ਸਿਰਫ਼ ਚਿੱਤਰ}one{ਸਿਰਫ਼ ਚਿੱਤਰ}other{ਸਿਰਫ਼ ਚਿੱਤਰ}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ਸਿਰਫ਼ ਵੀਡੀਓ}one{ਸਿਰਫ਼ ਵੀਡੀਓ}other{ਸਿਰਫ਼ ਵੀਡੀਓ}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ਸਿਰਫ਼ ਫ਼ਾਈਲ}one{ਸਿਰਫ਼ ਫ਼ਾਈਲ}other{ਸਿਰਫ਼ ਫ਼ਾਈਲਾਂ}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ਇਸ ਐਪ ਨੂੰ ਰਿਕਾਰਡ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਦਿੱਤੀ ਗਈ ਪਰ ਇਹ USB ਡੀਵਾਈਸ ਰਾਹੀਂ ਆਡੀਓ ਕੈਪਚਰ ਕਰ ਸਕਦੀ ਹੈ।"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ਨਿੱਜੀ"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"ਕੰਮ ਸੰਬੰਧੀ"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ਨਿੱਜੀ"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ਵਿਅਕਤੀਗਤ ਦ੍ਰਿਸ਼"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ਕਾਰਜ ਦ੍ਰਿਸ਼"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ਨਿੱਜੀ ਦ੍ਰਿਸ਼"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ਤੁਹਾਡੇ ਆਈ.ਟੀ. ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਬਲਾਕ ਕੀਤਾ ਗਿਆ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨਾਲ ਸਾਂਝਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨਾਲ ਨਹੀਂ ਖੋਲ੍ਹਿਆ ਜਾ ਸਕਦਾ"</string> diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml index 40fe5860..a9829275 100644 --- a/java/res/values-pl/strings.xml +++ b/java/res/values-pl/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{I jeszcze # plik}few{I jeszcze # pliki}many{I jeszcze # plików}other{I jeszcze # pliku}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Udostępnianie tekstu"</string>      <string name="sharing_link" msgid="2307694372813942916">"Udostępnianie linku"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Udostępnianie obrazu}few{Udostępnianie # obrazów}many{Udostępnianie # obrazów}other{Udostępnianie # obrazu}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Udostępniam obraz}few{Udostępniam # obrazy}many{Udostępniam # obrazów}other{Udostępniam # obrazu}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Udostępnianie filmu}few{Udostępnianie # filmów}many{Udostępnianie # filmów}other{Udostępnianie # filmu}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Udostępnianie # pliku}few{Udostępnianie # plików}many{Udostępnianie # plików}other{Udostępnianie # pliku}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Udostępnianie obrazu przez SMS}few{Udostępnianie # obrazów przez SMS}many{Udostępnianie # obrazów przez SMS}other{Udostępnianie # obrazu przez SMS}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Udostępnianie filmu przez link}few{Udostępnianie # filmów przez link}many{Udostępnianie # filmów przez link}other{Udostępnianie # filmu przez link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Udostępnianie pliku przez SMS}few{Udostępnianie # plików przez SMS}many{Udostępnianie # plików przez SMS}other{Udostępnianie # pliku przez SMS}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Udostępnianie pliku przez link}few{Udostępnianie # plików przez link}many{Udostępnianie # plików przez link}other{Udostępnianie # pliku przez link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Udostępnij album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tylko obraz}few{Tylko obrazy}many{Tylko obrazy}other{Tylko obrazy}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tylko film}few{Tylko filmy}many{Tylko filmy}other{Tylko filmy}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tylko plik}few{Tylko pliki}many{Tylko pliki}other{Tylko pliki}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacja nie ma uprawnień do nagrywania, ale może rejestrować dźwięk za pomocą tego urządzenia USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Osobiste"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Służbowe"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Prywatna"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Widok osobisty"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Widok służbowy"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Widok prywatny"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Działanie zablokowane przez administratora IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tych treści nie można udostępniać w aplikacjach służbowych"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tych treści nie można otworzyć w aplikacjach służbowych"</string> diff --git a/java/res/values-pt-rBR/strings.xml b/java/res/values-pt-rBR/strings.xml index ec52fd28..255fcbe6 100644 --- a/java/res/values-pt-rBR/strings.xml +++ b/java/res/values-pt-rBR/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Compartilhando texto"</string>      <string name="sharing_link" msgid="2307694372813942916">"Compartilhando link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhando imagem}one{Compartilhando # imagem}many{Compartilhando # de imagens}other{Compartilhando # imagens}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhar imagem}one{Compartilhar # imagem}many{Compartilhar # de imagens}other{Compartilhar # imagens}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartilhando vídeo}one{Compartilhando # vídeo}many{Compartilhando # de vídeos}other{Compartilhando # vídeos}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartilhando # arquivo}one{Compartilhando # arquivo}many{Compartilhando # de arquivos}other{Compartilhando # arquivos}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartilhando imagem com texto}one{Compartilhando # imagem com texto}many{Compartilhando # de imagens com texto}other{Compartilhando # imagens com texto}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartilhando vídeo com link}one{Compartilhando # vídeo com link}many{Compartilhando # de vídeos com link}other{Compartilhando # vídeos com link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartilhando arquivo com texto}one{Compartilhando # arquivo com texto}many{Compartilhando # de arquivos com texto}other{Compartilhando # arquivos com texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartilhando arquivo com link}one{Compartilhando # arquivo com link}many{Compartilhando # de arquivos com link}other{Compartilhando # arquivos com link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Compartilhando álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Somente imagem}one{Somente imagem}many{Somente imagens}other{Somente imagens}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Somente vídeo}one{Somente vídeo}many{Somente vídeos}other{Somente vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Somente arquivo}one{Somente arquivo}many{Somente arquivos}other{Somente arquivos}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Este app não tem permissão de gravação, mas pode capturar áudio pelo dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Particular"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualização pessoal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualização de trabalho"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualização particular"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Compartilhamento bloqueado pelo administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível compartilhar esse conteúdo com apps de trabalho"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string> diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml index c60b923b..4b44cb11 100644 --- a/java/res/values-pt-rPT/strings.xml +++ b/java/res/values-pt-rPT/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{A partilhar vídeo com link}many{A partilhar # vídeos com link}other{A partilhar # vídeos com link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{A partilhar ficheiro com texto}many{A partilhar # ficheiros com texto}other{A partilhar # ficheiros com texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{A partilhar ficheiro com link}many{A partilhar # ficheiros com link}other{A partilhar # ficheiros com link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Partilhar álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Apenas imagem}many{Apenas imagens}other{Apenas imagens}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Apenas vídeo}many{Apenas vídeos}other{Apenas vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Apenas ficheiro}many{Apenas ficheiros}other{Apenas ficheiros}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta app não recebeu autorização de gravação, mas pode capturar áudio através deste dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista pessoal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de trabalho"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado pelo administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível partilhar este conteúdo com apps de trabalho"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir este conteúdo com apps de trabalho"</string> diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml index ec52fd28..255fcbe6 100644 --- a/java/res/values-pt/strings.xml +++ b/java/res/values-pt/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Compartilhando texto"</string>      <string name="sharing_link" msgid="2307694372813942916">"Compartilhando link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhando imagem}one{Compartilhando # imagem}many{Compartilhando # de imagens}other{Compartilhando # imagens}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhar imagem}one{Compartilhar # imagem}many{Compartilhar # de imagens}other{Compartilhar # imagens}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartilhando vídeo}one{Compartilhando # vídeo}many{Compartilhando # de vídeos}other{Compartilhando # vídeos}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartilhando # arquivo}one{Compartilhando # arquivo}many{Compartilhando # de arquivos}other{Compartilhando # arquivos}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartilhando imagem com texto}one{Compartilhando # imagem com texto}many{Compartilhando # de imagens com texto}other{Compartilhando # imagens com texto}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartilhando vídeo com link}one{Compartilhando # vídeo com link}many{Compartilhando # de vídeos com link}other{Compartilhando # vídeos com link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartilhando arquivo com texto}one{Compartilhando # arquivo com texto}many{Compartilhando # de arquivos com texto}other{Compartilhando # arquivos com texto}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartilhando arquivo com link}one{Compartilhando # arquivo com link}many{Compartilhando # de arquivos com link}other{Compartilhando # arquivos com link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Compartilhando álbum"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Somente imagem}one{Somente imagem}many{Somente imagens}other{Somente imagens}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Somente vídeo}one{Somente vídeo}many{Somente vídeos}other{Somente vídeos}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Somente arquivo}one{Somente arquivo}many{Somente arquivos}other{Somente arquivos}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Este app não tem permissão de gravação, mas pode capturar áudio pelo dispositivo USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Particular"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualização pessoal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualização de trabalho"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualização particular"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Compartilhamento bloqueado pelo administrador de TI"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível compartilhar esse conteúdo com apps de trabalho"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string> diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml index d6cae158..1839e09a 100644 --- a/java/res/values-ro/strings.xml +++ b/java/res/values-ro/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Se trimite videoclipul cu linkul}few{Se trimit # videoclipuri cu linkul}other{Se trimit # de videoclipuri cu linkul}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Se trimite fișierul cu text}few{Se trimit # fișiere cu text}other{Se trimit # de fișiere cu text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Se trimite fișierul cu linkul}few{Se trimit # fișiere cu linkul}other{Se trimit # de fișiere cu linkul}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Se permite accesul la album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Numai imaginea}few{Numai imaginile}other{Numai imaginile}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Numai videoclipul}few{Numai videoclipurile}other{Numai videoclipurile}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Numai fișierul}few{Numai fișierele}other{Numai fișierele}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Permisiunea de înregistrare nu a fost acordată aplicației, dar aceasta poate să înregistreze conținut audio prin intermediul acestui dispozitiv USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Serviciu"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Afișarea conținutului personal"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Afișarea conținutului de lucru"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Secțiunea Privat"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocat de administratorul IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Acest conținut nu poate fi trimis cu aplicații pentru lucru"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Acest conținut nu poate fi deschis cu aplicații pentru lucru"</string> diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml index 618e0a6f..31d171ff 100644 --- a/java/res/values-ru/strings.xml +++ b/java/res/values-ru/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Отправка видео со ссылкой}one{Отправка # видео со ссылкой}few{Отправка # видео со ссылкой}many{Отправка # видео со ссылкой}other{Отправка # видео со ссылкой}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Отправка файла с текстом}one{Отправка # файла с текстом}few{Отправка # файлов с текстом}many{Отправка # файлов с текстом}other{Отправка # файла с текстом}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Отправка файла со ссылкой}one{Отправка # файла со ссылкой}few{Отправка # файлов со ссылкой}many{Отправка # файлов со ссылкой}other{Отправка # файла со ссылкой}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Вы делитесь альбомом"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Только изображение}one{Только изображения}few{Только изображения}many{Только изображения}other{Только изображения}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Только видео}one{Только видео}few{Только видео}many{Только видео}other{Только видео}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Только файл}one{Только файлы}few{Только файлы}many{Только файлы}other{Только файлы}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Приложению не разрешено записывать звук, однако оно может делать это с помощью этого USB-устройства."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Личное"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Рабочее"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Личное пространство"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Просмотр личных данных"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Просмотр рабочих данных"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Личное пространство"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Заблокировано вашим администратором"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Этим контентом нельзя делиться с рабочими приложениями."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Этот контент нельзя открыть в рабочем приложении."</string> diff --git a/java/res/values-si/strings.xml b/java/res/values-si/strings.xml index 176206e8..09418f55 100644 --- a/java/res/values-si/strings.xml +++ b/java/res/values-si/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{සබැඳිය සමග වීඩියෝව බෙදා ගැනීම}one{සබැඳිය සමග වීඩියෝ #ක් බෙදා ගැනීම}other{සබැඳිය සමග වීඩියෝ #ක් බෙදා ගැනීම}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{පෙළ සමග ගොනුව බෙදා ගැනීම}one{පෙළ සමග ගොනු #ක් බෙදා ගැනීම}other{පෙළ සමග ගොනු #ක් බෙදා ගැනීම}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{සබැඳිය සමග ගොනුව බෙදා ගැනීම}one{සබැඳිය සමග ගොනු #ක් බෙදා ගැනීම}other{සබැඳිය සමග ගොනු #ක් බෙදා ගැනීම}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ඇල්බමය බෙදා ගැනීම"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{රූපය පමණි}one{රූප පමණි}other{රූප පමණි}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{වීඩියෝව පමණි}one{වීඩියෝ පමණි}other{වීඩියෝ පමණි}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ගොනුව පමණි}one{ගොනු පමණි}other{ගොනු පමණි}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"මෙම යෙදුමට පටිගත කිරීම් අවසරයක් ලබා දී නොමැති නමුත් මෙම USB උපාංගය හරහා ශ්රව්ය ග්රහණය කර ගත හැකිය."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"පුද්ගලික"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"කාර්යාල"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"පෞද්ගලික"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"පෞද්ගලික දසුන"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"කාර්යාල දසුන"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"පෞද්ගලික දසුන"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ඔබගේ IT පරිපාලක විසින් අවහිර කර ඇත"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"මෙම අන්තර්ගතය කාර්යාල යෙදුම් සමඟ බෙදා ගත නොහැකිය"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"මෙම අන්තර්ගතය කාර්යාල යෙදුම් සමඟ විවෘත කළ නොහැකිය"</string> diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml index 1ac43e60..ef39351f 100644 --- a/java/res/values-sk/strings.xml +++ b/java/res/values-sk/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{a # ďalší súbor}few{a # ďalšie súbory}many{+ # more files}other{a # ďalších súborov}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Zdieľa sa textová správa"</string>      <string name="sharing_link" msgid="2307694372813942916">"Zdieľa sa odkaz"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Zdieľa sa obrázok}few{Zdieľajú sa # obrázky}many{Sharing # images}other{Zdieľa sa # obrázkov}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Zdieľanie obrázku}few{Zdieľanie # obrázkov}many{Sharing # images}other{Zdieľanie # obrázkov}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Zdieľa sa video}few{Zdieľajú sa # videá}many{Sharing # videos}other{Zdieľa sa # videí}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Zdieľa sa # súbor}few{Zdieľajú sa # súbory}many{Sharing # files}other{Zdieľa sa # súborov}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Zdieľa sa obrázok s textom}few{Zdieľajú sa # obrázky s textom}many{Sharing # images with text}other{Zdieľa sa # obrázkov s textom}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Zdieľa sa video s odkazom}few{Zdieľajú sa # videá s odkazom}many{Sharing # videos with link}other{Zdieľa sa # videí s odkazom}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Zdieľa sa súbor s textom}few{Zdieľajú sa # súbory s textom}many{Sharing # files with text}other{Zdieľa sa # súborov s textom}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Zdieľa sa súbor s odkazom}few{Zdieľajú sa # súbory s odkazom}many{Sharing # files with link}other{Zdieľa sa # súborov s odkazom}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Zdieľanie albumu"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Iba obrázok}few{Iba obrázky}many{Iba obrázky}other{Iba obrázky}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Iba video}few{Iba videá}many{Iba videá}other{Iba videá}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Iba súbor}few{Iba súbory}many{Iba súbory}other{Iba súbory}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tejto aplikácii nebolo udelené povolenie na nahrávanie, ale môže nasnímať zvuk cez toto zariadenie USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Osobné"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Pracovné"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Súkromné"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobné zobrazenie"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pracovné zobrazenie"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Súkromné zobrazenie"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokované vaším správcom IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tento obsah sa nedá zdieľať pomocou pracovných aplikácií"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah sa nedá otvoriť pomocou pracovných aplikácií"</string> diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml index 0ef88727..559cf3d1 100644 --- a/java/res/values-sl/strings.xml +++ b/java/res/values-sl/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deljenje videoposnetka s povezavo}one{Deljenje # videoposnetka s povezavo}two{Deljenje # videoposnetkov s povezavo}few{Deljenje # videoposnetkov s povezavo}other{Deljenje # videoposnetkov s povezavo}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deljenje datoteke z besedilom}one{Deljenje # datoteke z besedilom}two{Deljenje # datotek z besedilom}few{Deljenje # datotek z besedilom}other{Deljenje # datotek z besedilom}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deljenje datoteke s povezavo}one{Deljenje # datoteke s povezavo}two{Deljenje # datotek s povezavo}few{Deljenje # datotek s povezavo}other{Deljenje # datotek s povezavo}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Deljenje albuma"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}two{Samo slike}few{Samo slike}other{Samo slike}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videoposnetek}one{Samo videoposnetki}two{Samo videoposnetki}few{Samo videoposnetki}other{Samo videoposnetki}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo datoteka}one{Samo datoteke}two{Samo datoteke}few{Samo datoteke}other{Samo datoteke}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacija sicer nima dovoljenja za snemanje, vendar bi lahko zajemala zvok prek te naprave USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Osebno"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Delo"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Zasebno"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Pogled osebnega profila"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pogled delovnega profila"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Zasebni pogled"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokiral skrbnik za IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Te vsebine ni mogoče deliti z delovnimi aplikacijami."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Te vsebine ni mogoče odpreti z delovnimi aplikacijami."</string> diff --git a/java/res/values-sq/strings.xml b/java/res/values-sq/strings.xml index 95c3e57c..cd7fffee 100644 --- a/java/res/values-sq/strings.xml +++ b/java/res/values-sq/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Po ndahet një video me lidhje}other{Po ndahen # video me lidhje}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Po ndahet një skedar me tekst}other{Po ndahen # skedarë me tekst}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Po ndahet një skedar me lidhje}other{Po ndahen # skedarë me lidhje}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albumi po ndahet"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Vetëm imazhi}other{Vetëm imazhet}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vetëm videoja}other{Vetëm videot}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Vetëm skedari}other{Vetëm skedarët}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Këtij aplikacioni nuk i është dhënë leje për regjistrim, por mund të regjistrojë audio përmes kësaj pajisjeje USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Puna"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Pamja personale"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pamja e punës"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Pamja private"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bllokuar nga administratori yt i teknologjisë së informacionit"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Kjo përmbajtje nuk mund të ndahet me aplikacione pune"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kjo përmbajtje nuk mund të hapet me aplikacione pune"</string> diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml index 511a1293..a5d0e57b 100644 --- a/java/res/values-sr/strings.xml +++ b/java/res/values-sr/strings.xml @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ још # фајл}one{+ још # фајл}few{+ још # фајла}other{+ још # фајлова}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Дели се текст"</string>      <string name="sharing_link" msgid="2307694372813942916">"Дели се линк"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Дели се слика}one{Дели се # слика}few{Деле се # слике}other{Дели се # слика}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Дељење слике}one{Дељење # слике}few{Дељење # слике}other{Дељење # слика}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Дели се видео}one{Дели се # видео}few{Деле се # видео снимка}other{Дели се # видеа}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Дели се # фајл}one{Дели се # фајл}few{Деле се # фајла}other{Дели се # фајлова}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Дели се слика са текстом}one{Дели се # слика са текстом}few{Деле се # слике са текстом}other{Дели се # слика са текстом}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Дели се видео са линком}one{Дели се # видео са линком}few{Деле се # видео снимка са линком}other{Дели се # видеа са линком}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Дели се фајл са текстом}one{Дели се # фајл са текстом}few{Деле се # фајла са текстом}other{Дели се # фајлова са текстом}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Дели се фајл са линком}one{Дели се # фајл са линком}few{Деле се # фајла са линком}other{Дели се # фајлова са линком}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Дељени албум"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само слика}one{Само слике}few{Само слике}other{Само слике}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видео}one{Само видео снимци}few{Само видео снимци}other{Само видео снимци}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само фајл}one{Само фајлови}few{Само фајлови}other{Само фајлови}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ова апликација нема дозволу за снимање, али би могла да снима звук помоћу овог USB уређаја."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Лично"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Пословно"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Приватно"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Лични приказ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Приказ за посао"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватни приказ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Блокира ИТ администратор"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Овај садржај не може да се дели помоћу пословних апликација"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овај садржај не може да се отвара помоћу пословних апликација"</string> diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml index 7ed2d3f1..43492b9f 100644 --- a/java/res/values-sv/strings.xml +++ b/java/res/values-sv/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Delar video med länk}other{Delar # videor med länk}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Delar fil med text}other{Delar # filer med text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Delar fil med länk}other{Delar # filer med länk}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Delar album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Endast bild}other{Endast bilder}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Endast video}other{Endast videor}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Endast fil}other{Endast filer}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Appen har inte fått inspelningsbehörighet men kan spela in ljud via denna USB-enhet."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Privat"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Jobb"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personlig vy"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Jobbvy"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privat vy"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blockeras av IT-administratören"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Det här innehållet kan inte delas med jobbappar"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Det här innehållet kan inte öppnas med jobbappar"</string> diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml index de45a78c..74405a95 100644 --- a/java/res/values-sw/strings.xml +++ b/java/res/values-sw/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Inashiriki video na kiungo}other{Inashiriki video # na kiungo}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Inashiriki faili na maandishi}other{Inashiriki faili # na maandishi}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Inashiriki faili na kiungo}other{Inashiriki faili # na kiungo}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Kutumia albamu pamoja"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Picha pekee}other{Picha pekee}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video pekee}other{Video pekee}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Faili pekee}other{Faili pekee}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Programu hii haijapewa ruhusa ya kurekodi lakini inaweza kurekodi sauti kupitia kifaa hiki cha USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Binafsi"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Kazini"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Wa faragha"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Mwonekano wa binafsi"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Mwonekano wa kazini"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Mwonekano wa faragha"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Imezuiwa na msimamizi wako wa Tehama"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Huwezi kushiriki maudhui haya na programu za kazini"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Huwezi kufungua maudhui haya ukitumia programu za kazini"</string> diff --git a/java/res/values-ta/strings.xml b/java/res/values-ta/strings.xml index c95e5cb1..bef51c34 100644 --- a/java/res/values-ta/strings.xml +++ b/java/res/values-ta/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{இணைப்புடன் வீடியோவைப் பகிர்கிறது}other{இணைப்புடன் # வீடியோக்களைப் பகிர்கிறது}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{வார்த்தைகளைக் கொண்ட ஃபைலைப் பகிர்கிறது}other{வார்த்தைகளைக் கொண்ட # ஃபைல்களைப் பகிர்கிறது}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{இணைப்பைக் கொண்ட ஃபைலைப் பகிர்கிறது}other{இணைப்பைக் கொண்ட # ஃபைல்களைப் பகிர்கிறது}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ஆல்பத்தைப் பகிர்தல்"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{படம் மட்டும்}other{படங்கள் மட்டும்}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{வீடியோ மட்டும்}other{வீடியோக்கள் மட்டும்}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ஃபைல் மட்டும்}other{ஃபைல்கள் மட்டும்}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"இந்த ஆப்ஸிற்கு ரெக்கார்டு செய்வதற்கான அனுமதி வழங்கப்படவில்லை, எனினும் இந்த USB சாதனம் மூலம் ஆடியோவைப் பதிவுசெய்ய முடியும்."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"தனிப்பட்ட சுயவிவரம்"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"பணிச் சுயவிவரம்"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ரகசியம்"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"தனிப்பட்ட காட்சி"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"பணிக் காட்சி"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ரகசியக் காட்சி"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"இதை உங்கள் IT நிர்வாகி தடைசெய்துள்ளார்"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"பணி ஆப்ஸுடன் இந்த உள்ளடக்கத்தைப் பகிர முடியாது"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"பணி ஆப்ஸ் மூலம் இந்த உள்ளடக்கத்தைத் திறக்க முடியாது"</string> diff --git a/java/res/values-te/strings.xml b/java/res/values-te/strings.xml index a8b9457a..30f45be5 100644 --- a/java/res/values-te/strings.xml +++ b/java/res/values-te/strings.xml @@ -32,7 +32,7 @@      <string name="whichEditApplicationLabel" msgid="5992662938338600364">"ఎడిట్"</string>      <string name="whichSendApplication" msgid="59510564281035884">"షేర్ చేయండి"</string>      <string name="whichSendApplicationNamed" msgid="495577664218765855">"<xliff:g id="APP">%1$s</xliff:g> యాప్తో షేర్ చేయండి"</string> -    <string name="whichSendApplicationLabel" msgid="2391198069286568035">"షేర్ చేయి"</string> +    <string name="whichSendApplicationLabel" msgid="2391198069286568035">"షేర్ చేయండి"</string>      <string name="whichSendToApplication" msgid="2724450540348806267">"దీన్ని ఉపయోగించి పంపండి"</string>      <string name="whichSendToApplicationNamed" msgid="1996548940365954543">"<xliff:g id="APP">%1$s</xliff:g> యాప్ను ఉపయోగించి పంపండి"</string>      <string name="whichSendToApplicationLabel" msgid="6909037198280591110">"పంపండి"</string> @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ మరో # ఫైల్}other{+ మరో # ఫైల్స్}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"టెక్స్ట్ను షేర్ చేయడం"</string>      <string name="sharing_link" msgid="2307694372813942916">"లింక్ను షేర్ చేయడం"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ఇమేజ్ను షేర్ చేయడం}other{# ఇమేజ్లను షేర్ చేయడం}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ఈ ఇమేజ్ను షేర్ చేస్తున్నారు}other{ఈ # ఇమేజ్లను షేర్ చేస్తున్నారు}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{వీడియోను షేర్ చేయడం}other{# వీడియోలను షేర్ చేయడం}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ఫైల్ను షేర్ చేస్తోంది}other{# ఫైళ్లను షేర్ చేస్తోంది}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా ఇమేజ్ను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # ఇమేజ్లను షేర్ చేయడం}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{లింక్ చేయడం ద్వారా వీడియోను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # వీడియోలను షేర్ చేయడం}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా ఫైల్ను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # ఫైల్స్ను షేర్ చేయడం}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{లింక్ చేయడం ద్వారా ఫైల్ను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # ఫైల్స్ను షేర్ చేయడం}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"ఆల్బమ్ షేర్ చేయబడుతోంది"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ఇమేజ్ మాత్రమే}other{ఇమేజ్లు మాత్రమే}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{వీడియో మాత్రమే}other{వీడియోలు మాత్రమే}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ఫైల్ మాత్రమే}other{ఫైళ్లు మాత్రమే}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ఈ యాప్కు రికార్డ్ చేసే అనుమతి మంజూరు కాలేదు, అయినా ఈ USB పరికరం ద్వారా ఆడియోను క్యాప్చర్ చేయగలదు."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"వ్యక్తిగతం"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"వర్క్ ప్లేస్"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ప్రైవేట్"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"వ్యక్తిగత వీక్షణ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"పని వీక్షణ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ప్రైవేట్ వీక్షణ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"మీ IT అడ్మిన్ ద్వారా బ్లాక్ చేయబడింది"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ఈ కంటెంట్ వర్క్ యాప్తో షేర్ చేయడం సాధ్యం కాదు"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ఈ కంటెంట్ వర్క్ యాప్తో తెరవడం సాధ్యం కాదు"</string> diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml index af91064b..8db86fff 100644 --- a/java/res/values-th/strings.xml +++ b/java/res/values-th/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{กำลังแชร์วิดีโอพร้อมลิงก์}other{กำลังแชร์วิดีโอ # รายการพร้อมลิงก์}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{กำลังแชร์ไฟล์พร้อมข้อความ}other{กำลังแชร์ไฟล์ # รายการพร้อมข้อความ}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{กำลังแชร์ไฟล์พร้อมลิงก์}other{กำลังแชร์ไฟล์ # รายการพร้อมลิงก์}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"กำลังแชร์อัลบั้ม"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{รูปภาพเท่านั้น}other{รูปภาพเท่านั้น}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{วิดีโอเท่านั้น}other{วิดีโอเท่านั้น}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ไฟล์เท่านั้น}other{ไฟล์เท่านั้น}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"แอปนี้ไม่ได้รับอนุญาตให้บันทึกเสียงแต่อาจเก็บเสียงผ่านอุปกรณ์ USB นี้ได้"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ส่วนตัว"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"งาน"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"ส่วนตัว"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"มุมมองส่วนตัว"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ดูงาน"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"มุมมองส่วนตัว"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"ผู้ดูแลระบบไอทีบล็อกไว้"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"แชร์เนื้อหานี้โดยใช้แอปงานไม่ได้"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"เปิดเนื้อหานี้โดยใช้แอปงานไม่ได้"</string> diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml index cb4ff654..59d51005 100644 --- a/java/res/values-tl/strings.xml +++ b/java/res/values-tl/strings.xml @@ -32,7 +32,7 @@      <string name="whichEditApplicationLabel" msgid="5992662938338600364">"I-edit"</string>      <string name="whichSendApplication" msgid="59510564281035884">"Ibahagi"</string>      <string name="whichSendApplicationNamed" msgid="495577664218765855">"Ibahagi gamit ang <xliff:g id="APP">%1$s</xliff:g>"</string> -    <string name="whichSendApplicationLabel" msgid="2391198069286568035">"Ibahagi"</string> +    <string name="whichSendApplicationLabel" msgid="2391198069286568035">"I-share"</string>      <string name="whichSendToApplication" msgid="2724450540348806267">"Ipadala gamit ang"</string>      <string name="whichSendToApplicationNamed" msgid="1996548940365954543">"Ipadala gamit ang <xliff:g id="APP">%1$s</xliff:g>"</string>      <string name="whichSendToApplicationLabel" msgid="6909037198280591110">"Ipadala"</string> @@ -57,7 +57,7 @@      <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # pang file}one{+ # pang file}other{+ # pang file}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Ibinabahagi ang text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Ibinabahagi ang link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Ibinabahagi ang larawan}one{Ibinabahagi ang # larawan}other{Ibinabahagi ang # na larawan}}"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Shine-share ang larawan}one{Shine-share ang # larawan}other{Shine-share ang # na larawan}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Ibinabahagi ang video}one{Ibinabahagi ang # video}other{Ibinabahagi ang # na video}}"</string>      <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Nagshe-share ng # file}one{Nagshe-share ng # file}other{Nagshe-share ng # na file}}"</string>      <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Nagbabahagi ng larawang may text}one{Nagbabahagi ng # larawang may text}other{Nagbabahagi ng # na larawang may text}}"</string> @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Nagbabahagi ng video na may link}one{Nagbabahagi ng # video na may link}other{Nagbabahagi ng # na video na may link}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Nagbabahagi ng file na may text}one{Nagbabahagi ng # file na may text}other{Nagbabahagi ng # na file na may text}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Nagbabahagi ng file na may link}one{Nagbabahagi ng # file na may link}other{Nagbabahagi ng # na file na may link}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Ibinabahagi ang album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Larawan lang}one{Mga larawan lang}other{Mga larawan lang}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video lang}one{Mga video lang}other{Mga video lang}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File lang}one{Mga file lang}other{Mga file lang}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Hindi nabigyan ng pahintulot ang app na ito para mag-record pero nakakapag-capture ito ng audio sa pamamagitan ng USB device na ito."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Trabaho"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Pribado"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal na view"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"View ng trabaho"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Pribadong view"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Na-block ng iyong IT admin"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Hindi puwedeng ibahagi sa mga app para sa trabaho ang content na ito"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hindi puwedeng buksan sa mga app para sa trabaho ang content na ito"</string> diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml index 53d74bb9..eadfeeaa 100644 --- a/java/res/values-tr/strings.xml +++ b/java/res/values-tr/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bağlantı ekli video paylaşılıyor}other{Bağlantı ekli # video paylaşılıyor}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Metin ekli dosya paylaşılıyor}other{Metin ekli # dosya paylaşılıyor}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bağlantı ekli dosya paylaşılıyor}other{Bağlantı ekli # dosya paylaşılıyor}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albüm paylaşılıyor"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Yalnızca resim}other{Yalnızca resimler}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Yalnızca video}other{Yalnızca videolar}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Yalnızca dosya}other{Yalnızca dosyalar}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Bu uygulamaya ses kaydetme izni verilmedi ancak bu USB cihazı üzerinden sesleri yakalayabilir."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Kişisel"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"İş"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Gizli"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Kişisel görünüm"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"İş görünümü"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Gizli görünüm"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"BT yöneticiniz tarafından engellendi"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu içerik, iş uygulamalarıyla paylaşılamaz"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu içerik, iş uygulamalarıyla açılamaz"</string> diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml index f9d810af..a517db45 100644 --- a/java/res/values-uk/strings.xml +++ b/java/res/values-uk/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Надсилання відео з посиланням}one{Надсилання # відео з посиланням}few{Надсилання # відео з посиланням}many{Надсилання # відео з посиланням}other{Надсилання # відео з посиланням}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Надсилання файлу з текстом}one{Надсилання # файлу з текстом}few{Надсилання # файлів із текстом}many{Надсилання # файлів із текстом}other{Надсилання # файлу з текстом}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Надсилання файлу з посиланням}one{Надсилання # файлу з посиланням}few{Надсилання # файлів із посиланням}many{Надсилання # файлів із посиланням}other{Надсилання # файлу з посиланням}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Надання спільного доступу до альбома"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Лише зображення}one{Лише зображення}few{Лише зображення}many{Лише зображення}other{Лише зображення}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Лише відео}one{Лише відео}few{Лише відео}many{Лише відео}other{Лише відео}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Лише файл}one{Лише файли}few{Лише файли}many{Лише файли}other{Лише файли}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Цей додаток не має дозволу на запис, але він може фіксувати звук через цей USB-пристрій."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Особисте"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Робоче"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Приватний простір"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Особистий перегляд"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Робочий перегляд"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватний перегляд"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Заблоковано адміністратором"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Цим контентом не можна ділитися в робочих додатках"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Цей контент не можна відкривати в робочих додатках"</string> diff --git a/java/res/values-ur/strings.xml b/java/res/values-ur/strings.xml index 6a101d98..716a99af 100644 --- a/java/res/values-ur/strings.xml +++ b/java/res/values-ur/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{لنک کے ساتھ ویڈیو کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ٹیکسٹ کے ساتھ فائل کا اشتراک کیا جا رہا ہے}other{ٹیکسٹ کے ساتھ # فائلز کا اشتراک کیا جا رہا ہے}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{لنک کے ساتھ فائل کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # فائلز کا اشتراک کیا جا رہا ہے}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"البم کا اشتراک کیا جا رہا ہے"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{صرف تصویر}other{صرف تصاویر}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{صرف ویڈیو}other{صرف ویڈیوز}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{صرف فائل}other{صرف فائلز}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"اس ایپ کو ریکارڈ کرنے کی اجازت عطا نہیں کی گئی ہے مگر اس USB آلہ کے ذریعے آڈیو کیپچر کر سکتی ہے۔"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"ذاتی"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"دفتر"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"نجی"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ذاتی ملاحظہ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"دفتری ملاحظہ"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"نجی ملاحظہ"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"آپ کے IT منتظم کے ذریعے مسدود کردہ"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"اس مواد کا اشتراک ورک ایپس کے ساتھ نہیں کیا جا سکتا"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"اس مواد کو ورک ایپس کے ساتھ نہیں کھولا جا سکتا"</string> diff --git a/java/res/values-uz/strings.xml b/java/res/values-uz/strings.xml index 24249f50..d8e0bab7 100644 --- a/java/res/values-uz/strings.xml +++ b/java/res/values-uz/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Havolali videoni yuborish}other{# ta havolali videoni yuborish}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Matnli faylni yuborish}other{# ta matnli faylni yuborish}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Havolali faylni yuborish}other{# ta havolali faylni yuborish}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Albom ulashilmoqda"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Faqat rasm}other{Faqat rasmlar}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Faqat video}other{Faqat videolar}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Faqat fayl}other{Faqat fayllar}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Bu ilovaga yozib olish ruxsati berilmagan, lekin shu USB orqali ovozlarni yozib olishi mumkin."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Shaxsiy"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Ish"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Maxfiy"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Shaxsiy rejim"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ishchi rejim"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Maxfiy rejim"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Administratoringiz tomonidan bloklangan"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu kontent ishga oid ilovalar bilan ulashilmaydi"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontent ishga oid ilovalar bilan ochilmaydi"</string> diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml index b08d9a3a..a8d70cfc 100644 --- a/java/res/values-vi/strings.xml +++ b/java/res/values-vi/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Đang chia sẻ video có đường liên kết}other{Đang chia sẻ # video có đường liên kết}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Đang chia sẻ tệp có văn bản}other{Đang chia sẻ # tệp có văn bản}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Đang chia sẻ tệp có đường liên kết}other{Đang chia sẻ # tệp có đường liên kết}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"Chia sẻ album"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Chỉ chia sẻ hình ảnh}other{Chỉ chia sẻ các hình ảnh}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Chỉ chia sẻ video}other{Chỉ chia sẻ các video}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Chỉ chia sẻ tệp}other{Chỉ chia sẻ các tệp}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ứng dụng này chưa được cấp quyền ghi âm nhưng vẫn có thể ghi âm thông qua thiết bị USB này."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Cá nhân"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Công việc"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Riêng tư"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Chế độ xem cá nhân"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Chế độ xem công việc"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Chế độ xem riêng tư"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bị quản trị viên CNTT chặn"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bạn không thể chia sẻ nội dung này bằng ứng dụng công việc"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bạn không thể mở nội dung này bằng ứng dụng công việc"</string> diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml index e208e106..504bac6a 100644 --- a/java/res/values-zh-rCN/strings.xml +++ b/java/res/values-zh-rCN/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{正在分享带有链接的视频}other{正在分享带有链接的 # 个视频}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{正在分享带有文本的文件}other{正在分享带有文本的 # 个文件}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{正在分享带有链接的文件}other{正在分享带有链接的 # 个文件}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"分享影集"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{仅限图片}other{仅限图片}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{仅限视频}other{仅限视频}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{仅限文件}other{仅限文件}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"此应用未获得录音权限,但能通过此 USB 设备录制音频。"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"个人"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"私密"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"个人视图"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作视图"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私密视图"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"已被 IT 管理员禁止"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"无法使用工作应用分享该内容"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"无法使用工作应用打开该内容"</string> diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml index 837b1587..c54fc4b5 100644 --- a/java/res/values-zh-rHK/strings.xml +++ b/java/res/values-zh-rHK/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{正在分享影片 (含有連結)}other{正在分享 # 部影片 (含有連結)}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{正在分享檔案 (含有文字)}other{正在分享 # 個檔案 (含有文字)}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{正在分享檔案 (含有連結)}other{正在分享 # 個檔案 (含有連結)}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"共享相簿"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{僅含圖片}other{僅含圖片}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{僅含影片}other{僅含影片}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{僅含檔案}other{僅含檔案}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"此應用程式尚未獲授予錄音權限,但可透過此 USB 裝置記錄音訊。"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"個人"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"私人"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私人檢視模式"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"已被你的 IT 管理員封鎖"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"無法使用工作應用程式分享此內容"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"無法使用工作應用程式開啟此內容"</string> diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml index 0fddc70e..288602f4 100644 --- a/java/res/values-zh-rTW/strings.xml +++ b/java/res/values-zh-rTW/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{分享含有連結的影片}other{分享 # 部含有連結的影片}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{分享含有文字的檔案}other{分享含有文字的 # 個檔案}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{分享含有連結的檔案}other{分享含有連結的 # 個檔案}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"共享相簿"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{只有圖片}other{只有圖片}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{只有影片}other{只有影片}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{只有檔案}other{只有檔案}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"這個應用程式未取得錄製內容的權限,但可以透過這部 USB 裝置錄製音訊。"</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"個人"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"私人"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私人檢視模式"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT 管理員已封鎖這項操作"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"無法透過工作應用程式分享這項內容"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"無法使用工作應用程式開啟這項內容"</string> diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml index b651eb06..e30f51fd 100644 --- a/java/res/values-zu/strings.xml +++ b/java/res/values-zu/strings.xml @@ -66,6 +66,7 @@      <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Yabelana ngevidiyo ngelinki}one{Yabelana ngamavidiyo angu-# ngelinki}other{Yabelana ngamavidiyo angu-# ngelinki}}"</string>      <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Yabelana ngefayela ngombhalo}one{Yabelana ngamafayela angu-# ngombhalo}other{Yabelana ngamafayela angu-# ngombhalo}}"</string>      <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Yabelana ngefayela ngelinki}one{Yabelana ngamafayela angu-# ngelinki}other{Yabelana ngamafayela angu-# ngelinki}}"</string> +    <string name="sharing_album" msgid="191743129899503345">"I-albhamu eyabiwe"</string>      <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Isithombe kuphela}one{Izithombe kuphela}other{Izithombe kuphela}}"</string>      <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ividiyo kuphela}one{Amavidiyo kuphela}other{Amavidiyo kuphela}}"</string>      <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Ifayela kuphela}one{Amafayela kuphela}other{Amafayela kuphela}}"</string> @@ -76,8 +77,10 @@      <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Lolu hlelo lokusebenza alunikeziwe imvume yokurekhoda kodwa lungathwebula umsindo ngale divayisi ye-USB."</string>      <string name="resolver_personal_tab" msgid="1381052735324320565">"Okomuntu siqu"</string>      <string name="resolver_work_tab" msgid="3588325717455216412">"Umsebenzi"</string> +    <string name="resolver_private_tab" msgid="3707548826254095157">"Okuyimfihlo"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ukubuka komuntu siqu"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ukubuka komsebenzi"</string> +    <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ukubuka okuyimfihlo"</string>      <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Kuvinjelwe umlawuli wakho we-IT"</string>      <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Lokhu okuqukethwe akukwazi ukwabiwa nama-app womsebenzi"</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womsebenzi"</string> diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index 8843c81a..9d77d296 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -19,7 +19,6 @@      <!-- chooser/resolver (sharesheet) spacing -->      <dimen name="chooser_action_corner_radius">28dp</dimen>      <dimen name="chooser_action_horizontal_margin">2dp</dimen> -    <dimen name="chooser_action_max_width">200dp</dimen>      <dimen name="chooser_width">450dp</dimen>      <dimen name="chooser_corner_radius">28dp</dimen>      <dimen name="chooser_corner_radius_small">14dp</dimen> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 0c772573..5c1210b7 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -210,6 +210,11 @@          }      </string> + +    <!-- Title atop a sharing UI indicating that an album (typically of photos/videos) is being +         shared [CHAR_LIMIT=50] --> +    <string name="sharing_album">Sharing album</string> +      <!-- Message indicating that the attached text has been removed from this share and only the           images are being shared.  [CHAR LIMIT=none] -->      <string name="sharing_images_only">{count, plural, @@ -250,16 +255,20 @@       <!-- Prompt for the USB device resolver dialog with warning text for USB device dialogs.  [CHAR LIMIT=200] -->      <string name="usb_device_resolve_prompt_warn">This app has not been granted record permission but could capture audio through this USB device.</string> -    <!-- ResolverActivity - profile tabs --> +    <!-- ChooserActivity + ResolverActivity - profile tabs -->      <!-- Label of a tab on a screen. A user can tap this tap to switch to the 'Personal' view (that shows their personal content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->      <string name="resolver_personal_tab">Personal</string>      <!-- Label of a tab on a screen. A user can tap this tab to switch to the 'Work' view (that shows their work content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->      <string name="resolver_work_tab">Work</string> +    <!-- Label of a tab on a screen. A user can tap this tab to switch to the 'Private' view (that shows their Private Space content) if they have private space configured on their device. [CHAR LIMIT=NONE] --> +    <string name="resolver_private_tab">Private</string>      <!-- Accessibility label for the personal tab button. [CHAR LIMIT=NONE] -->      <string name="resolver_personal_tab_accessibility">Personal view</string>      <!-- Accessibility label for the work tab button. [CHAR LIMIT=NONE] -->      <string name="resolver_work_tab_accessibility">Work view</string> +    <!-- Accessibility label for the private tab button. [CHAR LIMIT=NONE] --> +    <string name="resolver_private_tab_accessibility">Private view</string>      <!-- Title of a screen. This text lets the user know that their IT admin doesn't allow them to share this content across profiles. [CHAR LIMIT=NONE] -->      <string name="resolver_cross_profile_blocked">Blocked by your IT admin</string> diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 9000ab3a..039fad56 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -125,7 +125,7 @@ import javax.inject.Inject;   */  @AndroidEntryPoint(ResolverActivity.class)  public class ChooserActivity extends Hilt_ChooserActivity implements -        ResolverListAdapter.ResolverListCommunicator { +        ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem {      private static final String TAG = "ChooserActivity";      /** @@ -259,7 +259,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  new AppPredictorFactory(                          this,                          mChooserRequest.getSharedText(), -                        mChooserRequest.getTargetIntentFilter()), +                        mChooserRequest.getTargetIntentFilter(), +                        getPackageManager().getAppPredictionServicePackageName() != null),                  mChooserRequest.getTargetIntentFilter()); @@ -302,14 +303,24 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          BasePreviewViewModel previewViewModel =                  new ViewModelProvider(this, createPreviewViewModelFactory())                          .get(BasePreviewViewModel.class); +        previewViewModel.init( +                mChooserRequest.getTargetIntent(), +                getIntent(), +                /*additionalContentUri = */ null, +                /*focusedItemIdx = */ 0, +                /*isPayloadTogglingEnabled = */ false);          mChooserContentPreviewUi = new ChooserContentPreviewUi(                  getCoroutineScope(getLifecycle()), -                previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()), +                previewViewModel.getPreviewDataProvider(),                  mChooserRequest.getTargetIntent(), -                previewViewModel.createOrReuseImageLoader(), +                previewViewModel.getImageLoader(),                  createChooserActionFactory(),                  mEnterTransitionAnimationDelegate, -                new HeadlineGeneratorImpl(this)); +                new HeadlineGeneratorImpl(this), +                ContentTypeHint.NONE, +                mChooserRequest.getMetadataText(), +                /*isPayloadTogglingEnabled =*/ false +        );          updateStickyContentPreview();          if (shouldShowStickyContentPreview() @@ -564,6 +575,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      /**       * Update UI to reflect changes in data.       */ +    @Override      public void handlePackagesChanged() {          handlePackagesChanged(/* listAdapter */ null);      } @@ -1206,13 +1218,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                              showTargetDetails(longPressedTargetInfo);                          }                      } - -                    @Override -                    public void updateProfileViewButton(View newButtonFromProfileRow) { -                        mProfileView = newButtonFromProfileRow; -                        mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); -                        ChooserActivity.this.updateProfileViewButton(); -                    }                  },                  chooserListAdapter,                  shouldShowContentPreview(), @@ -1252,7 +1257,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  maxTargetsPerRow,                  initialIntentsUserSpace,                  targetDataLoader, -                null); +                null, +                mFeatureFlags);      }      @Override @@ -1409,7 +1415,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;          int rowsToShow = gridAdapter.getSystemRowCount() -                + gridAdapter.getProfileRowCount()                  + gridAdapter.getServiceTargetRowCount()                  + gridAdapter.getCallerAndRankedTargetRowCount(); @@ -1657,8 +1662,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (!shouldShowContentPreview()) {              return false;          } -        boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle( -                UserHandle.of(UserHandle.myUserId())).getCount() == 0; +        ResolverListAdapter adapter = mMultiProfilePagerAdapter.getListAdapterForUserHandle( +                UserHandle.of(UserHandle.myUserId())); +        boolean isEmpty = adapter == null || adapter.getCount() == 0;          return (mFeatureFlags.scrollablePreview() || shouldShowTabs())                  && (!isEmpty || shouldShowContentPreviewWhenEmpty());      } diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 876ad5c3..5060f4f1 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -54,6 +54,7 @@ import com.android.intentresolver.chooser.SelectableTargetInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.icons.TargetDataLoader;  import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.widget.BadgeTextView;  import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; @@ -109,6 +110,7 @@ public class ChooserListAdapter extends ResolverListAdapter {      // Reserve spots for incoming direct share targets by adding placeholders      private final TargetInfo mPlaceHolderTargetInfo;      private final TargetDataLoader mTargetDataLoader; +    private final boolean mUseBadgeTextViewForLabels;      private final List<TargetInfo> mServiceTargets = new ArrayList<>();      private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>(); @@ -166,7 +168,8 @@ public class ChooserListAdapter extends ResolverListAdapter {              int maxRankedTargets,              UserHandle initialIntentsUserSpace,              TargetDataLoader targetDataLoader, -            @Nullable PackageChangeCallback packageChangeCallback) { +            @Nullable PackageChangeCallback packageChangeCallback, +            FeatureFlags featureFlags) {          this(                  context,                  payloadIntents, @@ -185,7 +188,8 @@ public class ChooserListAdapter extends ResolverListAdapter {                  targetDataLoader,                  packageChangeCallback,                  AsyncTask.SERIAL_EXECUTOR, -                context.getMainExecutor()); +                context.getMainExecutor(), +                featureFlags);      }      @VisibleForTesting @@ -207,7 +211,8 @@ public class ChooserListAdapter extends ResolverListAdapter {              TargetDataLoader targetDataLoader,              @Nullable PackageChangeCallback packageChangeCallback,              Executor bgExecutor, -            Executor mainExecutor) { +            Executor mainExecutor, +            FeatureFlags featureFlags) {          // Don't send the initial intents through the shared ResolverActivity path,          // we want to separate them into a different section.          super( @@ -231,6 +236,7 @@ public class ChooserListAdapter extends ResolverListAdapter {          mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);          mTargetDataLoader = targetDataLoader;          mPackageChangeCallback = packageChangeCallback; +        mUseBadgeTextViewForLabels = featureFlags.bespokeLabelView();          createPlaceHolders();          mEventLog = eventLog;          mShortcutSelectionLogic = new ShortcutSelectionLogic( @@ -332,7 +338,12 @@ public class ChooserListAdapter extends ResolverListAdapter {      @Override      View onCreateView(ViewGroup parent) { -        return mInflater.inflate(R.layout.resolve_grid_item, parent, false); +        return mInflater.inflate( +                mUseBadgeTextViewForLabels +                        ? R.layout.chooser_grid_item +                        : R.layout.resolve_grid_item, +                parent, +                false);      }      @VisibleForTesting @@ -340,7 +351,7 @@ public class ChooserListAdapter extends ResolverListAdapter {      public void onBindView(View view, TargetInfo info, int position) {          final ViewHolder holder = (ViewHolder) view.getTag(); -        holder.reset(); +        resetViewHolder(holder);          // Always remove the spacing listener, attach as needed to direct share targets below.          holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); @@ -377,16 +388,18 @@ public class ChooserListAdapter extends ResolverListAdapter {                      contentDescription,                      mContext.getResources().getString(R.string.pinned));              } -            holder.updateContentDescription(contentDescription); +            updateContentDescription(holder, contentDescription);              if (!info.hasDisplayIcon()) {                  loadDirectShareIcon((SelectableTargetInfo) info);              }          } else if (info.isDisplayResolveInfo()) {              if (info.isPinned()) { -                holder.updateContentDescription(String.join( -                        ". ", -                        info.getDisplayLabel(), -                        mContext.getResources().getString(R.string.pinned))); +                updateContentDescription( +                        holder, +                        String.join( +                                ". ", +                                info.getDisplayLabel(), +                                mContext.getResources().getString(R.string.pinned)));              }              DisplayResolveInfo dri = (DisplayResolveInfo) info;              if (!dri.hasDisplayIcon()) { @@ -398,22 +411,56 @@ public class ChooserListAdapter extends ResolverListAdapter {          }          if (info.isPlaceHolderTargetInfo()) { -            holder.bindPlaceholder(); +            bindPlaceholder(holder);          }          if (info.isMultiDisplayResolveInfo()) {              // If the target is grouped show an indicator -            holder.bindGroupIndicator( +            bindGroupIndicator( +                    holder,                      mContext.getDrawable(R.drawable.chooser_group_background));          } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD                  || getPositionTargetType(position) == TARGET_SERVICE)) {              // If the appShare or directShare target is pinned and in the suggested row show a              // pinned indicator -            holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background)); +            bindPinnedIndicator(holder, mContext.getDrawable(R.drawable.chooser_pinned_background));              holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);          }      } +    private void resetViewHolder(ViewHolder holder) { +        holder.reset(); +        holder.itemView.setBackground(holder.defaultItemViewBackground); + +        if (mUseBadgeTextViewForLabels) { +            ((BadgeTextView) holder.text).setBadgeDrawable(null); +        } +        holder.text.setBackground(null); +        holder.text.setPaddingRelative(0, 0, 0, 0); +    } + +    private void updateContentDescription(ViewHolder holder, String description) { +        holder.itemView.setContentDescription(description); +    } + +    private void bindPlaceholder(ViewHolder holder) { +        holder.itemView.setBackground(null); +    } + +    private void bindGroupIndicator(ViewHolder holder, Drawable indicator) { +        if (mUseBadgeTextViewForLabels) { +            ((BadgeTextView) holder.text).setBadgeDrawable(indicator); +        } else { +            holder.text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0); +            holder.text.setBackground(indicator); +        } +    } + +    private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) { +        holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0); +        holder.text.setBackground(indicator); +    } +      private void loadDirectShareIcon(SelectableTargetInfo info) {          if (mRequestedIcons.add(info)) {              mTargetDataLoader.loadDirectShareIcon( @@ -744,9 +791,6 @@ public class ChooserListAdapter extends ResolverListAdapter {              @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {          processSortedList(sortedComponents, doPostProcessing);          if (doPostProcessing) { -            mResolverListCommunicator.updateProfileViewButton(); -            //TODO: this method is different from super's only in that `notifyDataSetChanged` is -            // called conditionally here; is it really important?              notifyDataSetChanged();          }      } diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 7ad809e9..6c7f8264 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -16,6 +16,8 @@  package com.android.intentresolver; +import static java.util.Objects.requireNonNullElse; +  import android.content.ComponentName;  import android.content.Intent;  import android.content.IntentFilter; @@ -41,6 +43,8 @@ import java.net.URISyntaxException;  import java.util.ArrayList;  import java.util.Arrays;  import java.util.List; +import java.util.Objects; +import java.util.Optional;  import java.util.stream.Collector;  import java.util.stream.Collectors;  import java.util.stream.Stream; @@ -101,6 +105,9 @@ public class ChooserRequestParameters {      @Nullable      private final IntentFilter mTargetIntentFilter; +    @Nullable +    private final CharSequence mMetadataText; +      public ChooserRequestParameters(              final Intent clientIntent,              String referrerPackageName, @@ -125,8 +132,14 @@ public class ChooserRequestParameters {          mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer); -        mChosenComponentSender = clientIntent.getParcelableExtra( -                Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); +        mChosenComponentSender = +                Optional.ofNullable( +                        clientIntent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, +                                IntentSender.class)) +                        .orElse(clientIntent.getParcelableExtra( +                                Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER, +                                IntentSender.class)); +          mRefinementIntentSender = clientIntent.getParcelableExtra(                  Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); @@ -147,6 +160,12 @@ public class ChooserRequestParameters {          mChooserActions = getChooserActions(clientIntent);          mModifyShareAction = getModifyShareAction(clientIntent); + +        if (android.service.chooser.Flags.enableSharesheetMetadataExtra()) { +            mMetadataText = clientIntent.getCharSequenceExtra(Intent.EXTRA_METADATA_TEXT); +        } else { +            mMetadataText = null; +        }      }      public Intent getTargetIntent() { @@ -252,6 +271,11 @@ public class ChooserRequestParameters {          return mTargetIntentFilter;      } +    @Nullable +    public CharSequence getMetadataText() { +        return mMetadataText; +    } +      private static boolean isSendAction(@Nullable String action) {          return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));      } diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java index f0fcd149..30e69c18 100644 --- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java @@ -63,7 +63,7 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF      @Override      public void onClick(DialogInterface dialog, int which) {          mMultiDisplayResolveInfo.setSelected(which); -        ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true); +        ((StartsSelectedItem) getActivity()).startSelected(mParentWhich, false, true);          dismiss();      } diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java index b6b7de96..ae80fad4 100644 --- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java @@ -205,7 +205,7 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment          } else {              pinComponent(mTargetInfos.get(which).getResolvedComponentName());          } -        ((ChooserActivity) getActivity()).handlePackagesChanged(); +        ((PackagesChangedListener) getActivity()).handlePackagesChanged();          dismiss();      } diff --git a/java/src/com/android/intentresolver/ContentTypeHint.kt b/java/src/com/android/intentresolver/ContentTypeHint.kt new file mode 100644 index 00000000..f607e4ae --- /dev/null +++ b/java/src/com/android/intentresolver/ContentTypeHint.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver + +import android.content.Intent + +/** Enum reflecting the value of [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT]. */ +enum class ContentTypeHint { +    NONE, +    ALBUM, +} diff --git a/java/src/com/android/intentresolver/PackagesChangedListener.kt b/java/src/com/android/intentresolver/PackagesChangedListener.kt new file mode 100644 index 00000000..10f0bf51 --- /dev/null +++ b/java/src/com/android/intentresolver/PackagesChangedListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver + +/** A component which can be notified when packages have changed. */ +interface PackagesChangedListener { +    /** Report that packages have changed. */ +    fun handlePackagesChanged() +} diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 564d8d19..80d07d2c 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -25,7 +25,6 @@ import android.content.pm.ResolveInfo;  import android.graphics.ColorMatrix;  import android.graphics.ColorMatrixColorFilter;  import android.graphics.drawable.Drawable; -import android.net.Uri;  import android.os.AsyncTask;  import android.os.RemoteException;  import android.os.Trace; @@ -477,9 +476,6 @@ public class ResolverListAdapter extends BaseAdapter {              @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {          processSortedList(sortedComponents, doPostProcessing);          notifyDataSetChanged(); -        if (doPostProcessing) { -            mResolverListCommunicator.updateProfileViewButton(); -        }      }      protected void processSortedList( @@ -651,6 +647,7 @@ public class ResolverListAdapter extends BaseAdapter {          return null;      } +    @Override      public int getCount() {          int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :                  mDisplayList.size(); @@ -664,6 +661,7 @@ public class ResolverListAdapter extends BaseAdapter {          return mDisplayList.size();      } +    @Override      @Nullable      public TargetInfo getItem(int position) {          if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { @@ -676,6 +674,7 @@ public class ResolverListAdapter extends BaseAdapter {          }      } +    @Override      public long getItemId(int position) {          return position;      } @@ -693,6 +692,7 @@ public class ResolverListAdapter extends BaseAdapter {          return mDisplayList.get(index);      } +    @Override      public final View getView(int position, View convertView, ViewGroup parent) {          View view = convertView;          if (view == null) { @@ -753,9 +753,7 @@ public class ResolverListAdapter extends BaseAdapter {      }      private void onIconLoaded(DisplayResolveInfo displayResolveInfo, Drawable drawable) { -        if (getOtherProfile() == displayResolveInfo) { -            mResolverListCommunicator.updateProfileViewButton(); -        } else if (!displayResolveInfo.hasDisplayIcon()) { +        if (!displayResolveInfo.hasDisplayIcon()) {              displayResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);              notifyDataSetChanged();          } @@ -903,14 +901,18 @@ public class ResolverListAdapter extends BaseAdapter {          Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); +        // ResolverListCommunicator +        default void updateProfileViewButton() { +        } +          void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,                  boolean rebuildCompleted);          void sendVoiceChoicesIfNeeded(); -        void updateProfileViewButton(); - -        boolean useLayoutWithDefault(); +        default boolean useLayoutWithDefault() { +            return false; +        }          boolean shouldGetActivityMetadata(); @@ -918,7 +920,9 @@ public class ResolverListAdapter extends BaseAdapter {           * @return true to filter only apps that can handle           *     {@link android.content.Intent#CATEGORY_DEFAULT} intents           */ -        default boolean shouldGetOnlyDefaultActivities() { return true; }; +        default boolean shouldGetOnlyDefaultActivities() { +            return true; +        }          void onHandlePackagesChanged(ResolverListAdapter listAdapter);      } @@ -930,7 +934,7 @@ public class ResolverListAdapter extends BaseAdapter {      @VisibleForTesting      public static class ViewHolder {          public View itemView; -        public Drawable defaultItemViewBackground; +        public final Drawable defaultItemViewBackground;          public TextView text;          public TextView text2; @@ -940,8 +944,6 @@ public class ResolverListAdapter extends BaseAdapter {              text.setText("");              text.setMaxLines(2);              text.setMaxWidth(Integer.MAX_VALUE); -            text.setBackground(null); -            text.setPaddingRelative(0, 0, 0, 0);              text2.setVisibility(View.GONE);              text2.setText(""); @@ -982,10 +984,6 @@ public class ResolverListAdapter extends BaseAdapter {              itemView.setContentDescription(null);          } -        public void updateContentDescription(String description) { -            itemView.setContentDescription(description); -        } -          /**           * Bind view holder to a TargetInfo.           */ @@ -998,19 +996,5 @@ public class ResolverListAdapter extends BaseAdapter {                  icon.setColorFilter(null);              }          } - -        public void bindPlaceholder() { -            itemView.setBackground(null); -        } - -        public void bindGroupIndicator(Drawable indicator) { -            text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0); -            text.setBackground(indicator); -        } - -        public void bindPinnedIndicator(Drawable indicator) { -            text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0); -            text.setBackground(indicator); -        }      }  } diff --git a/java/src/com/android/intentresolver/StartsSelectedItem.kt b/java/src/com/android/intentresolver/StartsSelectedItem.kt new file mode 100644 index 00000000..01cdf124 --- /dev/null +++ b/java/src/com/android/intentresolver/StartsSelectedItem.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver + +interface StartsSelectedItem { +    /** Start the selected item. */ +    fun startSelected(which: Int, always: Boolean, filtered: Boolean) +} diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java index b97e6b45..4fe28384 100644 --- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java @@ -17,9 +17,11 @@  package com.android.intentresolver.chooser;  import android.app.Activity; +import android.content.ComponentName;  import android.content.Intent;  import android.os.Bundle;  import android.os.UserHandle; +import android.util.Log;  import androidx.annotation.Nullable; @@ -121,6 +123,19 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {      }      @Override +    public ComponentName getResolvedComponentName() { +        if (hasSelected()) { +            return mTargetInfos.get(mSelected).getResolvedComponentName(); +        } +        // It is not expected to have this method be called on an unselected multi-display item. +        // Call super to preserve the legacy (most likely erroneous) behavior. +        Log.wtf( +                "ChooserActivity", +                "retrieving ResolvedComponentName from an unselected MultiDisplayResolveInfo"); +        return super.getResolvedComponentName(); +    } + +    @Override      public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {          return mTargetInfos.get(mSelected).startAsUser(activity, options, user);      } diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt index 10ee5af1..21c909ea 100644 --- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt @@ -17,16 +17,22 @@  package com.android.intentresolver.contentpreview  import android.content.Intent +import android.net.Uri  import androidx.annotation.MainThread  import androidx.lifecycle.ViewModel -import com.android.intentresolver.ChooserRequestParameters  /** A contract for the preview view model. Added for testing. */  abstract class BasePreviewViewModel : ViewModel() { -    @MainThread -    abstract fun createOrReuseProvider( -        targetIntent: Intent -    ): PreviewDataProvider +    @get:MainThread abstract val previewDataProvider: PreviewDataProvider +    @get:MainThread abstract val imageLoader: ImageLoader +    abstract val payloadToggleInteractor: PayloadToggleInteractor? -    @MainThread abstract fun createOrReuseImageLoader(): ImageLoader +    @MainThread +    abstract fun init( +        targetIntent: Intent, +        chooserIntent: Intent, +        additionalContentUri: Uri?, +        focusedItemIdx: Int, +        isPayloadTogglingEnabled: Boolean, +    )  } diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index a015147d..6f201ad5 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview;  import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;  import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; +import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION;  import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;  import android.content.ClipData; @@ -32,6 +33,7 @@ import android.view.ViewGroup;  import androidx.annotation.Nullable;  import androidx.annotation.VisibleForTesting; +import com.android.intentresolver.ContentTypeHint;  import com.android.intentresolver.widget.ActionRow;  import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; @@ -48,6 +50,7 @@ import kotlinx.coroutines.CoroutineScope;  public final class ChooserContentPreviewUi {      private final CoroutineScope mScope; +    private final boolean mIsPayloadTogglingEnabled;      /**       * Delegate to build the default system action buttons to display in the preview layout, if/when @@ -98,8 +101,13 @@ public final class ChooserContentPreviewUi {              ImageLoader imageLoader,              ActionFactory actionFactory,              TransitionElementStatusCallback transitionElementStatusCallback, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            ContentTypeHint contentTypeHint, +            @Nullable CharSequence metadata, +            // TODO: replace with the FeatureFlag ref when v1 is gone +            boolean isPayloadTogglingEnabled) {          mScope = scope; +        mIsPayloadTogglingEnabled = isPayloadTogglingEnabled;          mContentPreviewUi = createContentPreview(                  previewData,                  targetIntent, @@ -107,7 +115,10 @@ public final class ChooserContentPreviewUi {                  imageLoader,                  actionFactory,                  transitionElementStatusCallback, -                headlineGenerator); +                headlineGenerator, +                contentTypeHint, +                metadata +        );          if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) {              transitionElementStatusCallback.onAllTransitionElementsReady();          } @@ -120,8 +131,10 @@ public final class ChooserContentPreviewUi {              ImageLoader imageLoader,              ActionFactory actionFactory,              TransitionElementStatusCallback transitionElementStatusCallback, -            HeadlineGenerator headlineGenerator) { - +            HeadlineGenerator headlineGenerator, +            ContentTypeHint contentTypeHint, +            @Nullable CharSequence metadata +    ) {          int previewType = previewData.getPreviewType();          if (previewType == CONTENT_PREVIEW_TEXT) {              return createTextPreview( @@ -129,20 +142,31 @@ public final class ChooserContentPreviewUi {                      targetIntent,                      actionFactory,                      imageLoader, -                    headlineGenerator); +                    headlineGenerator, +                    contentTypeHint, +                    metadata +            );          }          if (previewType == CONTENT_PREVIEW_FILE) {              FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi(                      previewData.getUriCount(),                      actionFactory, -                    headlineGenerator); +                    headlineGenerator, +                    metadata +            );              if (previewData.getUriCount() > 0) {                  previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName);              }              return fileContentPreviewUi;          } + +        if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION && mIsPayloadTogglingEnabled) { +            transitionElementStatusCallback.onAllTransitionElementsReady(); // TODO +            return new ShareouselContentPreviewUi(actionFactory); +        } +          boolean isSingleImageShare = previewData.getUriCount() == 1 -                        && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType()); +                && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType());          CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);          if (!TextUtils.isEmpty(text)) {              FilesPlusTextContentPreviewUi previewUi = @@ -155,7 +179,9 @@ public final class ChooserContentPreviewUi {                              actionFactory,                              imageLoader,                              typeClassifier, -                            headlineGenerator); +                            headlineGenerator, +                            metadata +                    );              if (previewData.getUriCount() > 0) {                  JavaFlowHelper.collectToList(                          mScope, @@ -175,7 +201,9 @@ public final class ChooserContentPreviewUi {                  transitionElementStatusCallback,                  previewData.getImagePreviewFileInfoFlow(),                  previewData.getUriCount(), -                headlineGenerator); +                headlineGenerator, +                metadata +        );      }      public int getPreferredContentPreview() { @@ -200,7 +228,10 @@ public final class ChooserContentPreviewUi {              Intent targetIntent,              ChooserContentPreviewUi.ActionFactory actionFactory,              ImageLoader imageLoader, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            ContentTypeHint contentTypeHint, +            @Nullable CharSequence metadata +    ) {          CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);          CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE);          ClipData previewData = targetIntent.getClipData(); @@ -211,13 +242,16 @@ public final class ChooserContentPreviewUi {                  previewThumbnail = previewDataItem.getUri();              }          } +          return new TextContentPreviewUi(                  scope,                  sharingText,                  previewTitle, +                metadata,                  previewThumbnail,                  actionFactory,                  imageLoader, -                headlineGenerator); +                headlineGenerator, +                contentTypeHint);      }  } diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java index ad1c6c01..79bb9d3c 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java @@ -25,11 +25,13 @@ import java.lang.annotation.Retention;  @Retention(SOURCE)  @IntDef({ContentPreviewType.CONTENT_PREVIEW_FILE,          ContentPreviewType.CONTENT_PREVIEW_IMAGE, -        ContentPreviewType.CONTENT_PREVIEW_TEXT}) +        ContentPreviewType.CONTENT_PREVIEW_TEXT, +        ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION})  public @interface ContentPreviewType {      // Starting at 1 since 0 is considered "undefined" for some of the database transformations      // of tron logs.      int CONTENT_PREVIEW_IMAGE = 1;      int CONTENT_PREVIEW_FILE = 2;      int CONTENT_PREVIEW_TEXT = 3; +    int CONTENT_PREVIEW_PAYLOAD_SELECTION = 4;  } diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index dce146b0..b0fb278e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -30,12 +30,14 @@ import android.widget.ImageView;  import android.widget.TextView;  import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting;  import com.android.intentresolver.R;  import com.android.intentresolver.widget.ActionRow;  import com.android.intentresolver.widget.ScrollableImagePreviewView; -abstract class ContentPreviewUi { +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public abstract class ContentPreviewUi {      private static final int IMAGE_FADE_IN_MILLIS = 150;      static final String TAG = "ChooserPreview"; @@ -83,6 +85,19 @@ abstract class ContentPreviewUi {          }      } +    protected static void displayMetadata(View layout, @Nullable CharSequence metadata) { +        TextView metadataView = layout == null ? null : layout.findViewById(R.id.metadata); +        if (metadataView == null) { +            return; +        } +        if (!TextUtils.isEmpty(metadata)) { +            metadataView.setText(metadata); +            metadataView.setVisibility(View.VISIBLE); +        } else { +            metadataView.setVisibility(View.GONE); +        } +    } +      protected static void displayModifyShareAction(              View layout, ChooserContentPreviewUi.ActionFactory actionFactory) {          ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction(); diff --git a/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt new file mode 100644 index 00000000..6a12f56c --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/CursorUriReader.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.ContentInterface +import android.content.Intent +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.os.Bundle +import android.os.CancellationSignal +import android.service.chooser.AdditionalContentContract.Columns +import android.service.chooser.AdditionalContentContract.CursorExtraKeys +import android.util.Log +import android.util.SparseArray +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.coroutineScope + +private const val TAG = ContentPreviewUi.TAG + +/** + * A bi-directional cursor reader. Reads URI from the [cursor] starting from the given [startPos], + * filters items by [predicate]. + */ +class CursorUriReader( +    private val cursor: Cursor, +    startPos: Int, +    private val pageSize: Int, +    private val predicate: (Uri) -> Boolean, +) : PayloadToggleInteractor.CursorReader { +    override val count = cursor.count +    // Unread ranges are: +    // - left: [0, leftPos); +    // - right: [rightPos, count) +    // i.e. read range is: [leftPos, rightPos) +    private var rightPos = startPos.coerceIn(0, count) +    private var leftPos = rightPos + +    override val hasMoreBefore +        get() = leftPos > 0 + +    override val hasMoreAfter +        get() = rightPos < count + +    override fun readPageAfter(): SparseArray<Uri> { +        if (!hasMoreAfter) return SparseArray() +        if (!cursor.moveToPosition(rightPos)) { +            rightPos = count +            Log.w(TAG, "Failed to move the cursor to position $rightPos, stop reading the cursor") +            return SparseArray() +        } +        val result = SparseArray<Uri>(pageSize) +        do { +            cursor +                .getString(0) +                ?.let(Uri::parse) +                ?.takeIf { predicate(it) } +                ?.let { uri -> result.append(rightPos, uri) } +            rightPos++ +        } while (result.size() < pageSize && cursor.moveToNext()) +        maybeCloseCursor() +        return result +    } + +    override fun readPageBefore(): SparseArray<Uri> { +        if (!hasMoreBefore) return SparseArray() +        val startPos = maxOf(0, leftPos - pageSize) +        if (!cursor.moveToPosition(startPos)) { +            leftPos = 0 +            Log.w(TAG, "Failed to move the cursor to position $startPos, stop reading cursor") +            return SparseArray() +        } +        val result = SparseArray<Uri>(leftPos - startPos) +        for (pos in startPos until leftPos) { +            cursor +                .getString(0) +                ?.let(Uri::parse) +                ?.takeIf { predicate(it) } +                ?.let { uri -> result.append(pos, uri) } +            if (!cursor.moveToNext()) break +        } +        leftPos = startPos +        maybeCloseCursor() +        return result +    } + +    private fun maybeCloseCursor() { +        if (!hasMoreBefore && !hasMoreAfter) { +            close() +        } +    } + +    override fun close() { +        cursor.close() +    } + +    companion object { +        suspend fun createCursorReader( +            contentResolver: ContentInterface, +            uri: Uri, +            chooserIntent: Intent +        ): CursorUriReader { +            val cancellationSignal = CancellationSignal() +            val cursor = +                try { +                    coroutineScope { +                        runCatching { +                                contentResolver.query( +                                    uri, +                                    arrayOf(Columns.URI), +                                    Bundle().apply { +                                        putParcelable(Intent.EXTRA_INTENT, chooserIntent) +                                    }, +                                    cancellationSignal +                                ) +                            } +                            .getOrNull() +                            ?: MatrixCursor(arrayOf(Columns.URI)) +                    } +                } catch (e: CancellationException) { +                    cancellationSignal.cancel() +                    throw e +                } +            return CursorUriReader( +                cursor, +                cursor.extras?.getInt(CursorExtraKeys.POSITION, 0) ?: 0, +                128, +            ) { +                it.authority != uri.authority +            } +        } +    } +} diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 89e7e528..d4eea8b9 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -43,15 +43,20 @@ class FileContentPreviewUi extends ContentPreviewUi {      private final ChooserContentPreviewUi.ActionFactory mActionFactory;      private final HeadlineGenerator mHeadlineGenerator;      @Nullable +    private final CharSequence mMetadata; +    @Nullable      private ViewGroup mContentPreview = null;      FileContentPreviewUi(              int fileCount,              ChooserContentPreviewUi.ActionFactory actionFactory, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            @Nullable CharSequence metadata +    ) {          mFileCount = fileCount;          mActionFactory = actionFactory;          mHeadlineGenerator = headlineGenerator; +        mMetadata = metadata;      }      @Override @@ -91,6 +96,7 @@ class FileContentPreviewUi extends ContentPreviewUi {          inflateHeadline(headlineViewParent);          displayHeadline(headlineViewParent, mHeadlineGenerator.getFilesHeadline(mFileCount)); +        displayMetadata(headlineViewParent, mMetadata);          if (mFileCount == 0) {              mContentPreview.setVisibility(View.GONE); diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java index 78fc6586..6832c5c4 100644 --- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -57,6 +57,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {      private final ImageLoader mImageLoader;      private final MimeTypeClassifier mTypeClassifier;      private final HeadlineGenerator mHeadlineGenerator; +    @Nullable +    private final CharSequence mMetadata;      private final boolean mIsSingleImage;      private final int mFileCount;      private ViewGroup mContentPreviewView; @@ -78,7 +80,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {              ChooserContentPreviewUi.ActionFactory actionFactory,              ImageLoader imageLoader,              MimeTypeClassifier typeClassifier, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            @Nullable CharSequence metadata) {          if (isSingleImage && fileCount != 1) {              throw new IllegalArgumentException(                      "fileCount = " + fileCount + " and isSingleImage = true"); @@ -92,6 +95,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {          mImageLoader = imageLoader;          mTypeClassifier = typeClassifier;          mHeadlineGenerator = headlineGenerator; +        mMetadata = metadata;      }      @Override @@ -204,6 +208,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {          }          displayHeadline(headlineView, headline); +        displayMetadata(headlineView, mMetadata);      }      private void prepareTextPreview( diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt index 5f87c924..21308341 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt @@ -17,12 +17,14 @@  package com.android.intentresolver.contentpreview  /** - * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief - * description of the content being shared. + * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief description + * of the content being shared.   */  interface HeadlineGenerator {      fun getTextHeadline(text: CharSequence): String +    fun getAlbumHeadline(): String +      fun getImagesWithTextHeadline(text: CharSequence, count: Int): String      fun getVideosWithTextHeadline(text: CharSequence, count: Int): String diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt index ef1e55d8..6e126822 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -34,6 +34,10 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {          )      } +    override fun getAlbumHeadline(): String { +        return context.getString(R.string.sharing_album) +    } +      override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String {          return getPluralString(              getTemplateResource( diff --git a/java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt b/java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt new file mode 100644 index 00000000..1cc1a6a6 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/MutableActionFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.service.chooser.ChooserAction +import com.android.intentresolver.widget.ActionRow +import kotlinx.coroutines.flow.Flow + +interface MutableActionFactory { +    /** A flow of custom actions */ +    val customActionsFlow: Flow<List<ActionRow.Action>> + +    /** Update custom actions */ +    fun updateCustomActions(actions: List<ChooserAction>) +} diff --git a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt new file mode 100644 index 00000000..eda5c4ca --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt @@ -0,0 +1,382 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.Intent +import android.content.IntentSender +import android.net.Uri +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import android.util.Log +import android.util.SparseArray +import java.io.Closeable +import java.util.LinkedList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.BufferOverflow.DROP_LATEST +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +private const val TAG = "PayloadToggleInteractor" + +@OptIn(ExperimentalCoroutinesApi::class) +class PayloadToggleInteractor( +    // TODO: a single-thread dispatcher is currently expected. iterate on the synchronization logic. +    private val scope: CoroutineScope, +    private val initiallySharedUris: List<Uri>, +    private val focusedUriIdx: Int, +    private val mimeTypeClassifier: MimeTypeClassifier, +    private val cursorReaderProvider: suspend () -> CursorReader, +    private val uriMetadataReader: (Uri) -> FileInfo, +    private val targetIntentModifier: (List<Item>) -> Intent, +    private val selectionCallback: (Intent) -> ShareouselUpdate?, +) { +    private var cursorDataRef = CompletableDeferred<CursorData?>() +    private val records = LinkedList<Record>() +    private val prevPageLoadingGate = AtomicBoolean(true) +    private val nextPageLoadingGate = AtomicBoolean(true) +    private val notifySelectionJobRef = AtomicReference<Job?>() +    private val emptyState = +        State( +            emptyList(), +            hasMoreItemsBefore = false, +            hasMoreItemsAfter = false, +            allowSelectionChange = false +        ) + +    private val stateFlowSource = MutableStateFlow(emptyState) + +    val customActions = +        MutableSharedFlow<List<ChooserAction>>(replay = 1, onBufferOverflow = DROP_LATEST) + +    val stateFlow: Flow<State> +        get() = stateFlowSource.filter { it !== emptyState } + +    val targetPosition: Flow<Int> = stateFlow.map { it.targetPos } +    val previewKeys: Flow<List<Item>> = stateFlow.map { it.items } + +    fun getKey(item: Any): Int = (item as Item).key + +    fun selected(key: Item): Flow<Boolean> = (key as Record).isSelected + +    fun previewUri(key: Item): Flow<Uri?> = flow { emit(key.previewUri) } + +    fun previewInteractor(key: Any): PayloadTogglePreviewInteractor { +        val state = stateFlowSource.value +        if (state === emptyState) { +            Log.wtf(TAG, "Requesting item preview before any item has been published") +        } else { +            if (state.hasMoreItemsBefore && key === state.items.firstOrNull()) { +                loadMorePreviousItems() +            } +            if (state.hasMoreItemsAfter && key == state.items.lastOrNull()) { +                loadMoreNextItems() +            } +        } +        return PayloadTogglePreviewInteractor(key as Item, this) +    } + +    init { +        scope +            .launch { awaitCancellation() } +            .invokeOnCompletion { +                cursorDataRef.cancel() +                runCatching { +                        if (cursorDataRef.isCompleted && !cursorDataRef.isCancelled) { +                            cursorDataRef.getCompleted() +                        } else { +                            null +                        } +                    } +                    .getOrNull() +                    ?.reader +                    ?.close() +            } +    } + +    fun start() { +        scope.launch { +            val cursorReader = cursorReaderProvider() +            val selectedItems = +                initiallySharedUris.map { uri -> +                    val fileInfo = uriMetadataReader(uri) +                    Record( +                        0, // artificial key for the pending record, it should not be used anywhere +                        uri, +                        fileInfo.previewUri, +                        fileInfo.mimeType, +                    ) +                } +            val cursorData = +                CursorData( +                    cursorReader, +                    SelectionTracker(selectedItems, focusedUriIdx, cursorReader.count) { uri }, +                ) +            if (cursorDataRef.complete(cursorData)) { +                doLoadMorePreviousItems() +                val startPos = records.size +                doLoadMoreNextItems() +                prevPageLoadingGate.set(false) +                nextPageLoadingGate.set(false) +                publishSnapshot(startPos) +            } else { +                cursorReader.close() +            } +        } +    } + +    fun loadMorePreviousItems() { +        invokeAsyncIfNotRunning(prevPageLoadingGate) { +            doLoadMorePreviousItems() +            publishSnapshot() +        } +    } + +    fun loadMoreNextItems() { +        invokeAsyncIfNotRunning(nextPageLoadingGate) { +            doLoadMoreNextItems() +            publishSnapshot() +        } +    } + +    fun setSelected(item: Item, isSelected: Boolean) { +        val record = item as Record +        scope.launch { +            val (_, selectionTracker) = waitForCursorData() ?: return@launch +            if (selectionTracker.setItemSelection(record.key, record, isSelected)) { +                val targetIntent = targetIntentModifier(selectionTracker.getSelection()) +                val newJob = scope.launch { notifySelectionChanged(targetIntent) } +                notifySelectionJobRef.getAndSet(newJob)?.cancel() +                record.isSelected.value = selectionTracker.isItemSelected(record.key) +            } +        } +    } + +    private fun invokeAsyncIfNotRunning(guardingFlag: AtomicBoolean, block: suspend () -> Unit) { +        if (guardingFlag.compareAndSet(false, true)) { +            scope.launch { block() }.invokeOnCompletion { guardingFlag.set(false) } +        } +    } + +    private suspend fun doLoadMorePreviousItems() { +        val (reader, selectionTracker) = waitForCursorData() ?: return +        if (!reader.hasMoreBefore) return + +        val newItems = reader.readPageBefore().toItems() +        selectionTracker.onStartItemsAdded(newItems) +        for (i in newItems.size() - 1 downTo 0) { +            records.add( +                0, +                (newItems.valueAt(i) as Record).apply { +                    isSelected.value = selectionTracker.isItemSelected(key) +                } +            ) +        } +        if (!reader.hasMoreBefore && !reader.hasMoreAfter) { +            val pendingItems = selectionTracker.getPendingItems() +            val newRecords = +                pendingItems.foldIndexed(SparseArray<Item>()) { idx, acc, item -> +                    assert(item is Record) { "Unexpected pending item type: ${item.javaClass}" } +                    val rec = item as Record +                    val key = idx - pendingItems.size +                    acc.append( +                        key, +                        Record( +                            key, +                            rec.uri, +                            rec.previewUri, +                            rec.mimeType, +                            rec.mimeType?.mimeTypeToItemType() ?: ItemType.File +                        ) +                    ) +                    acc +                } + +            selectionTracker.onStartItemsAdded(newRecords) +            for (i in (newRecords.size() - 1) downTo 0) { +                records.add(0, (newRecords.valueAt(i) as Record).apply { isSelected.value = true }) +            } +        } +    } + +    private suspend fun doLoadMoreNextItems() { +        val (reader, selectionTracker) = waitForCursorData() ?: return +        if (!reader.hasMoreAfter) return + +        val newItems = reader.readPageAfter().toItems() +        selectionTracker.onEndItemsAdded(newItems) +        for (i in 0 until newItems.size()) { +            val key = newItems.keyAt(i) +            records.add( +                (newItems.valueAt(i) as Record).apply { +                    isSelected.value = selectionTracker.isItemSelected(key) +                } +            ) +        } +        if (!reader.hasMoreBefore && !reader.hasMoreAfter) { +            val items = +                selectionTracker.getPendingItems().let { items -> +                    items.foldIndexed(SparseArray<Item>(items.size)) { i, acc, item -> +                        val key = reader.count + i +                        val record = item as Record +                        acc.append( +                            key, +                            Record(key, record.uri, record.previewUri, record.mimeType, record.type) +                        ) +                        acc +                    } +                } +            selectionTracker.onEndItemsAdded(items) +            for (i in 0 until items.size()) { +                records.add((items.valueAt(i) as Record).apply { isSelected.value = true }) +            } +        } +    } + +    private fun SparseArray<Uri>.toItems(): SparseArray<Item> { +        val items = SparseArray<Item>(size()) +        for (i in 0 until size()) { +            val key = keyAt(i) +            val uri = valueAt(i) +            val fileInfo = uriMetadataReader(uri) +            items.append( +                key, +                Record( +                    key, +                    uri, +                    fileInfo.previewUri, +                    fileInfo.mimeType, +                    fileInfo.mimeType?.mimeTypeToItemType() ?: ItemType.File +                ) +            ) +        } +        return items +    } + +    private suspend fun waitForCursorData() = cursorDataRef.await() + +    private fun notifySelectionChanged(targetIntent: Intent) { +        selectionCallback(targetIntent)?.customActions?.let { customActions.tryEmit(it) } +    } + +    private suspend fun publishSnapshot(startPos: Int = -1) { +        val (reader, _) = waitForCursorData() ?: return +        // TODO: publish a view into the list as it can only grow on each side thus a view won't be +        // invalidated +        val items = ArrayList<Item>(records) +        stateFlowSource.emit( +            State( +                items, +                reader.hasMoreBefore, +                reader.hasMoreAfter, +                allowSelectionChange = true, +                targetPos = startPos, +            ) +        ) +    } + +    private fun String.mimeTypeToItemType(): ItemType = +        when { +            mimeTypeClassifier.isImageType(this) -> ItemType.Image +            mimeTypeClassifier.isVideoType(this) -> ItemType.Video +            else -> ItemType.File +        } + +    class State( +        val items: List<Item>, +        val hasMoreItemsBefore: Boolean, +        val hasMoreItemsAfter: Boolean, +        val allowSelectionChange: Boolean, +        val targetPos: Int = -1, +    ) + +    sealed interface Item { +        val key: Int +        val uri: Uri +        val previewUri: Uri? +        val mimeType: String? +        val type: ItemType +    } + +    enum class ItemType { +        Image, +        Video, +        File, +    } + +    private class Record( +        override val key: Int, +        override val uri: Uri, +        override val previewUri: Uri? = uri, +        override val mimeType: String?, +        override val type: ItemType = ItemType.Image, +    ) : Item { +        val isSelected = MutableStateFlow(false) +    } + +    data class ShareouselUpdate( +        // for all properties, null value means no change +        val customActions: List<ChooserAction>? = null, +        val modifyShareAction: ChooserAction? = null, +        val alternateIntents: List<Intent>? = null, +        val callerTargets: List<ChooserTarget>? = null, +        val refinementIntentSender: IntentSender? = null, +    ) + +    private data class CursorData( +        val reader: CursorReader, +        val selectionTracker: SelectionTracker<Item>, +    ) + +    interface CursorReader : Closeable { +        val count: Int +        val hasMoreBefore: Boolean +        val hasMoreAfter: Boolean + +        fun readPageAfter(): SparseArray<Uri> + +        fun readPageBefore(): SparseArray<Uri> +    } +} + +class PayloadTogglePreviewInteractor( +    private val item: PayloadToggleInteractor.Item, +    private val interactor: PayloadToggleInteractor, +) { +    fun setSelected(selected: Boolean) { +        interactor.setSelected(item, selected) +    } + +    val previewUri: Flow<Uri?> +        get() = interactor.previewUri(item) + +    val selected: Flow<Boolean> +        get() = interactor.selected(item) + +    val key +        get() = item.key +} diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt index 38918d79..96bb8258 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -18,7 +18,6 @@ package com.android.intentresolver.contentpreview  import android.content.ContentInterface  import android.content.Intent -import android.database.Cursor  import android.media.MediaMetadata  import android.net.Uri  import android.provider.DocumentsContract @@ -31,6 +30,7 @@ import androidx.annotation.OpenForTesting  import androidx.annotation.VisibleForTesting  import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE  import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE +import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION  import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT  import com.android.intentresolver.measurements.runTracing  import com.android.intentresolver.util.ownedByCurrentUser @@ -74,7 +74,11 @@ open class PreviewDataProvider  constructor(      private val scope: CoroutineScope,      private val targetIntent: Intent, +    private val additionalContentUri: Uri?,      private val contentResolver: ContentInterface, +    // TODO: replace with the ChooserServiceFlags ref when PreviewViewModel dependencies are sorted +    // out +    private val isPayloadTogglingEnabled: Boolean,      private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier,  ) { @@ -100,6 +104,9 @@ constructor(      open val uriCount: Int          get() = records.size +    val uris: List<Uri> +        get() = records.map { it.uri } +      /**       * Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and       * [FileInfo.previewUri] set (a data projection tailored for the image preview UI). @@ -122,6 +129,9 @@ constructor(               * IMAGE, FILE, TEXT. */              if (!targetIntent.isSend || records.isEmpty()) {                  CONTENT_PREVIEW_TEXT +            } else if (isPayloadTogglingEnabled && shouldShowPayloadSelection()) { +                // TODO: replace with the proper flags injection +                CONTENT_PREVIEW_PAYLOAD_SELECTION              } else {                  try {                      runBlocking(scope.coroutineContext) { @@ -140,6 +150,22 @@ constructor(          }      } +    private fun shouldShowPayloadSelection(): Boolean { +        val extraContentUri = additionalContentUri ?: return false +        return runCatching { +                val authority = extraContentUri.authority +                records.firstOrNull { authority == it.uri.authority } == null +            } +            .onFailure { +                Log.w( +                    ContentPreviewUi.TAG, +                    "Failed to check URI authorities; no payload toggling", +                    it +                ) +            } +            .getOrDefault(false) +    } +      /**       * The first shared URI's metadata. This call wait's for the data to be loaded and falls back to       * a crude value if the data is not loaded within a time limit. @@ -250,8 +276,7 @@ constructor(          val isImageType: Boolean              get() = typeClassifier.isImageType(mimeType)          val supportsImageType: Boolean by lazy { -            contentResolver.getStreamTypesSafe(uri)?.firstOrNull(typeClassifier::isImageType) != -                null +            contentResolver.getStreamTypesSafe(uri).firstOrNull(typeClassifier::isImageType) != null          }          val supportsThumbnail: Boolean              get() = query.supportsThumbnail @@ -263,7 +288,8 @@ constructor(          private val query by lazy { readQueryResult() }          private fun readQueryResult(): QueryResult = -            contentResolver.querySafe(uri)?.use { cursor -> +            // TODO: rewrite using methods from UiMetadataHelpers.kt +            contentResolver.querySafe(uri, METADATA_COLUMNS)?.use { cursor ->                  if (!cursor.moveToFirst()) return@use null                  var flagColIdx = -1 @@ -344,51 +370,3 @@ private fun getFileName(uri: Uri): String {          fileName.substring(index + 1)      }  } - -private fun ContentInterface.getTypeSafe(uri: Uri): String? = -    runTracing("getType") { -        try { -            getType(uri) -        } catch (e: SecurityException) { -            logProviderPermissionWarning(uri, "mime type") -            null -        } catch (t: Throwable) { -            Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) -            null -        } -    } - -private fun ContentInterface.getStreamTypesSafe(uri: Uri): Array<String>? = -    runTracing("getStreamTypes") { -        try { -            getStreamTypes(uri, "*/*") -        } catch (e: SecurityException) { -            logProviderPermissionWarning(uri, "stream types") -            null -        } catch (t: Throwable) { -            Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: $uri", t) -            null -        } -    } - -private fun ContentInterface.querySafe(uri: Uri): Cursor? = -    runTracing("query") { -        try { -            query(uri, METADATA_COLUMNS, null, null) -        } catch (e: SecurityException) { -            logProviderPermissionWarning(uri, "metadata") -            null -        } catch (t: Throwable) { -            Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) -            null -        } -    } - -private fun logProviderPermissionWarning(uri: Uri, dataName: String) { -    // The ContentResolver already logs the exception. Log something more informative. -    Log.w( -        ContentPreviewUi.TAG, -        "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" + -            " ensure that the sharesheet is given permission." -    ) -} diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 6350756e..d694c6ff 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -17,58 +17,108 @@  package com.android.intentresolver.contentpreview  import android.app.Application +import android.content.ContentResolver  import android.content.Intent +import android.net.Uri  import androidx.annotation.MainThread  import androidx.lifecycle.ViewModel  import androidx.lifecycle.ViewModelProvider  import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY  import androidx.lifecycle.viewModelScope  import androidx.lifecycle.viewmodel.CreationExtras -import com.android.intentresolver.ChooserRequestParameters  import com.android.intentresolver.R  import com.android.intentresolver.inject.Background -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +import java.util.concurrent.Executors  import kotlinx.coroutines.CoroutineDispatcher  import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher  import kotlinx.coroutines.plus -/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */ -@HiltViewModel -class PreviewViewModel -@Inject -constructor( -    private val application: Application, +/** A view model for the preview logic */ +class PreviewViewModel( +    private val contentResolver: ContentResolver, +    // TODO: inject ImageLoader instead +    private val thumbnailSize: Int,      @Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,  ) : BasePreviewViewModel() { -    private var previewDataProvider: PreviewDataProvider? = null -    private var imageLoader: ImagePreviewImageLoader? = null +    private var targetIntent: Intent? = null +    private var chooserIntent: Intent? = null +    private var additionalContentUri: Uri? = null +    private var focusedItemIdx: Int = 0 +    private var isPayloadTogglingEnabled = false -    @MainThread -    override fun createOrReuseProvider( -        targetIntent: Intent -    ): PreviewDataProvider = -        previewDataProvider -            ?: PreviewDataProvider( -                    viewModelScope + dispatcher, -                    targetIntent, -                    application.contentResolver -                ) -                .also { previewDataProvider = it } +    override val previewDataProvider by lazy { +        val targetIntent = requireNotNull(this.targetIntent) { "Not initialized" } +        PreviewDataProvider( +            viewModelScope + dispatcher, +            targetIntent, +            additionalContentUri, +            contentResolver, +            isPayloadTogglingEnabled, +        ) +    } + +    override val imageLoader by lazy { +        ImagePreviewImageLoader( +            viewModelScope + dispatcher, +            thumbnailSize, +            contentResolver, +            cacheSize = 16 +        ) +    } + +    override val payloadToggleInteractor: PayloadToggleInteractor? by lazy { +        val targetIntent = requireNotNull(targetIntent) { "Not initialized" } +        // TODO: replace with flags injection +        if (!isPayloadTogglingEnabled) return@lazy null +        createPayloadToggleInteractor( +                additionalContentUri ?: return@lazy null, +                targetIntent, +                chooserIntent ?: return@lazy null, +            ) +            .apply { start() } +    } +    // TODO: make the view model injectable and inject these dependencies instead      @MainThread -    override fun createOrReuseImageLoader(): ImageLoader = -        imageLoader -            ?: ImagePreviewImageLoader( -                    viewModelScope + dispatcher, -                    thumbnailSize = -                        application.resources.getDimensionPixelSize( -                            R.dimen.chooser_preview_image_max_dimen -                        ), -                    application.contentResolver, -                    cacheSize = 16 +    override fun init( +        targetIntent: Intent, +        chooserIntent: Intent, +        additionalContentUri: Uri?, +        focusedItemIdx: Int, +        isPayloadTogglingEnabled: Boolean, +    ) { +        if (this.targetIntent != null) return +        this.targetIntent = targetIntent +        this.chooserIntent = chooserIntent +        this.additionalContentUri = additionalContentUri +        this.focusedItemIdx = focusedItemIdx +        this.isPayloadTogglingEnabled = isPayloadTogglingEnabled +    } + +    private fun createPayloadToggleInteractor( +        contentProviderUri: Uri, +        targetIntent: Intent, +        chooserIntent: Intent, +    ): PayloadToggleInteractor { +        return PayloadToggleInteractor( +            // TODO: update PayloadToggleInteractor to support multiple threads +            viewModelScope + Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher(), +            previewDataProvider.uris, +            maxOf(0, minOf(focusedItemIdx, previewDataProvider.uriCount - 1)), +            DefaultMimeTypeClassifier, +            { +                CursorUriReader.createCursorReader( +                    contentResolver, +                    contentProviderUri, +                    chooserIntent                  ) -                .also { imageLoader = it } +            }, +            UriMetadataReader(contentResolver, DefaultMimeTypeClassifier), +            TargetIntentModifier(targetIntent, getUri = { uri }, getMimeType = { mimeType }), +            SelectionChangeCallback(contentProviderUri, chooserIntent, contentResolver) +        ) +    }      companion object {          val Factory: ViewModelProvider.Factory = @@ -77,7 +127,16 @@ constructor(                  override fun <T : ViewModel> create(                      modelClass: Class<T>,                      extras: CreationExtras -                ): T = PreviewViewModel(checkNotNull(extras[APPLICATION_KEY])) as T +                ): T { +                    val application: Application = checkNotNull(extras[APPLICATION_KEY]) +                    return PreviewViewModel( +                        application.contentResolver, +                        application.resources.getDimensionPixelSize( +                            R.dimen.chooser_preview_image_max_dimen +                        ) +                    ) +                        as T +                }              }      }  } diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt new file mode 100644 index 00000000..6b33e1cd --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.ContentInterface +import android.content.Intent +import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION +import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_TARGETS +import android.content.Intent.EXTRA_INTENT +import android.content.IntentSender +import android.net.Uri +import android.os.Bundle +import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import com.android.intentresolver.contentpreview.PayloadToggleInteractor.ShareouselUpdate +import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents +import com.android.intentresolver.v2.ui.viewmodel.readChooserActions +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.log +import com.android.intentresolver.v2.validation.types.array +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +private const val TAG = "SelectionChangeCallback" + +/** + * Encapsulates payload change callback invocation to the sharing app; handles callback arguments + * and result format mapping. + */ +class SelectionChangeCallback( +    private val uri: Uri, +    private val chooserIntent: Intent, +    private val contentResolver: ContentInterface, +) : (Intent) -> ShareouselUpdate? { +    fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? = +        contentResolver +            .call( +                requireNotNull(uri.authority) { "URI authority can not be null" }, +                ON_SELECTION_CHANGED, +                uri.toString(), +                Bundle().apply { +                    putParcelable( +                        EXTRA_INTENT, +                        Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) } +                    ) +                } +            ) +            ?.let { bundle -> +                return when (val result = readCallbackResponse(bundle)) { +                    is Valid -> result.value +                    is Invalid -> { +                        result.errors.forEach { it.log(TAG) } +                        null +                    } +                } +            } + +    override fun invoke(targetIntent: Intent) = onSelectionChanged(targetIntent) + +    private fun readCallbackResponse(bundle: Bundle): ValidationResult<ShareouselUpdate> { +        return validateFrom(bundle::get) { +            val customActions = readChooserActions() +            val modifyShareAction = +                optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) +            val alternateIntents = readAlternateIntents() +            val callerTargets = optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) +            val refinementIntentSender = +                optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + +            ShareouselUpdate( +                customActions, +                modifyShareAction, +                alternateIntents, +                callerTargets, +                refinementIntentSender, +            ) +        } +    } +} diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt b/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt new file mode 100644 index 00000000..c9431731 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/SelectionTracker.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.net.Uri +import android.util.SparseArray +import android.util.SparseIntArray +import androidx.core.util.containsKey +import androidx.core.util.isNotEmpty + +/** + * Tracks selected items (including those that has not been read frm the cursor) and their relative + * order. + */ +class SelectionTracker<Item>( +    selectedItems: List<Item>, +    private val focusedItemIdx: Int, +    private val cursorCount: Int, +    private val getUri: Item.() -> Uri, +) { +    /** Contains selected items keys. */ +    private val selections = SparseArray<Item>(selectedItems.size) + +    /** +     * A set of initially selected items that has not yet been observed by the lazy read of the +     * cursor and thus has unknown key (cursor position). Initially, all [selectedItems] are put in +     * this map with items at the index less than [focusedItemIdx] with negative keys (to the left +     * of all cursor items) and items at the index more or equal to [focusedItemIdx] with keys more +     * or equal to [cursorCount] (to the right of all cursor items) in their relative order. Upon +     * reading the cursor, [onEndItemsAdded]/[onStartItemsAdded], all pending items from that +     * collection in the corresponding direction get their key assigned and gets removed from the +     * map. Items that were missing from the cursor get removed from the map by +     * [getPendingItems] + [onStartItemsAdded]/[onEndItemsAdded] combination. +     */ +    private val pendingKeys = HashMap<Uri, SparseIntArray>() + +    init { +        selectedItems.forEachIndexed { i, item -> +            // all items before focusedItemIdx gets "positioned" before all the cursor items +            // and all the reset after all the cursor items in their relative order. +            // Also see the comments to pendingKeys property. +            val key = +                if (i < focusedItemIdx) { +                    i - focusedItemIdx +                } else { +                    i + cursorCount - focusedItemIdx +                } +            selections.append(key, item) +            pendingKeys.getOrPut(item.getUri()) { SparseIntArray(1) }.append(key, key) +        } +    } + +    /** Update selections based on the set of items read from the end of the cursor */ +    fun onEndItemsAdded(items: SparseArray<Item>) { +        for (i in 0 until items.size()) { +            val item = items.valueAt(i) +            pendingKeys[item.getUri()] +                // if only one pending (unmatched) item with this URI is left, removed this URI +                ?.also { +                    if (it.size() <= 1) { +                        pendingKeys.remove(item.getUri()) +                    } +                } +                // a safeguard, we should not observe empty arrays at this point +                ?.takeIf { it.isNotEmpty() } +                // pick a matching pending items from the right side +                ?.let { pendingUriPositions -> +                    val key = items.keyAt(i) +                    val insertPos = +                        pendingUriPositions +                            .findBestKeyPosition(key) +                            .coerceIn(0, pendingUriPositions.size() - 1) +                    // select next pending item from the right, if not such item exists then +                    // the data is inconsistent and we pick the closes one from the left +                    val keyPlaceholder = pendingUriPositions.keyAt(insertPos) +                    pendingUriPositions.removeAt(insertPos) +                    selections.remove(keyPlaceholder) +                    selections[key] = item +                } +        } +    } + +    /** Update selections based on the set of items read from the head of the cursor */ +    fun onStartItemsAdded(items: SparseArray<Item>) { +        for (i in (items.size() - 1) downTo 0) { +            val item = items.valueAt(i) +            pendingKeys[item.getUri()] +                // if only one pending (unmatched) item with this URI is left, removed this URI +                ?.also { +                    if (it.size() <= 1) { +                        pendingKeys.remove(item.getUri()) +                    } +                } +                // a safeguard, we should not observe empty arrays at this point +                ?.takeIf { it.isNotEmpty() } +                // pick a matching pending items from the left side +                ?.let { pendingUriPositions -> +                    val key = items.keyAt(i) +                    val insertPos = +                        pendingUriPositions +                            .findBestKeyPosition(key) +                            .coerceIn(1, pendingUriPositions.size()) +                    // select next pending item from the left, if not such item exists then +                    // the data is inconsistent and we pick the closes one from the right +                    val keyPlaceholder = pendingUriPositions.keyAt(insertPos - 1) +                    pendingUriPositions.removeAt(insertPos - 1) +                    selections.remove(keyPlaceholder) +                    selections[key] = item +                } +        } +    } + +    /** Updated selection status for the given item */ +    fun setItemSelection(key: Int, item: Item, isSelected: Boolean): Boolean { +        val idx = selections.indexOfKey(key) +        if (isSelected && idx < 0) { +            selections[key] = item +            return true +        } +        if (!isSelected && idx >= 0 && selections.size() > 1) { +            selections.removeAt(idx) +            return true +        } +        return false +    } + +    /** Return selection status for the given item */ +    fun isItemSelected(key: Int): Boolean = selections.containsKey(key) + +    fun getSelection(): List<Item> = +        buildList(selections.size()) { +            for (i in 0 until selections.size()) { +                add(selections.valueAt(i)) +            } +        } + +    /** Return all selected items that has not yet been read from the cursor */ +    fun getPendingItems(): List<Item> = +        if (pendingKeys.isEmpty()) { +            emptyList() +        } else { +            buildList { +                for (i in 0 until selections.size()) { +                    val item = selections.valueAt(i) ?: continue +                    if (isPending(item, selections.keyAt(i))) { +                        add(item) +                    } +                } +            } +        } + +    private fun isPending(item: Item, key: Int): Boolean { +        val keys = pendingKeys[item.getUri()] ?: return false +        return keys.containsKey(key) +    } + +    private fun SparseIntArray.findBestKeyPosition(key: Int): Int = +        // undocumented, but indexOfKey behaves in the same was as +        // java.util.Collections#binarySearch() +        indexOfKey(key).let { if (it < 0) it.inv() else it } +} diff --git a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt new file mode 100644 index 00000000..82c09986 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.contentpreview + +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.intentresolver.R +import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory +import com.android.intentresolver.contentpreview.shareousel.ui.composable.Shareousel +import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselViewModel +import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.toShareouselViewModel + +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +class ShareouselContentPreviewUi( +    private val actionFactory: ActionFactory, +) : ContentPreviewUi() { + +    override fun getType(): Int = ContentPreviewType.CONTENT_PREVIEW_IMAGE + +    override fun display( +        resources: Resources, +        layoutInflater: LayoutInflater, +        parent: ViewGroup, +        headlineViewParent: View?, +    ): ViewGroup { +        return displayInternal(parent, headlineViewParent).also { layout -> +            displayModifyShareAction(headlineViewParent ?: layout, actionFactory) +        } +    } + +    private fun displayInternal( +        parent: ViewGroup, +        headlineViewParent: View?, +    ): ViewGroup { +        if (headlineViewParent != null) { +            inflateHeadline(headlineViewParent) +        } +        val composeView = +            ComposeView(parent.context).apply { +                setContent { +                    val vm: BasePreviewViewModel = viewModel() +                    val interactor = +                        requireNotNull(vm.payloadToggleInteractor) { "Should not be null" } + +                    var viewModel by remember { mutableStateOf<ShareouselViewModel?>(null) } +                    LaunchedEffect(Unit) { +                        viewModel = +                            interactor.toShareouselViewModel( +                                vm.imageLoader, +                                actionFactory, +                                vm.viewModelScope +                            ) +                    } + +                    headlineViewParent?.let { +                        viewModel?.let { viewModel -> +                            LaunchedEffect(viewModel) { +                                viewModel.headline.collect { headline -> +                                    headlineViewParent +                                        .findViewById<TextView>(R.id.headline) +                                        ?.apply { +                                            if (headline.isNotBlank()) { +                                                text = headline +                                                visibility = View.VISIBLE +                                            } else { +                                                visibility = View.GONE +                                            } +                                        } +                                } +                            } +                        } +                    } + +                    viewModel?.let { viewModel -> +                        MaterialTheme( +                            colorScheme = +                                if (isSystemInDarkTheme()) { +                                    dynamicDarkColorScheme(LocalContext.current) +                                } else { +                                    dynamicLightColorScheme(LocalContext.current) +                                }, +                        ) { +                            Shareousel(viewModel = viewModel) +                        } +                    } +                        ?: run { +                            Spacer( +                                Modifier.height( +                                    dimensionResource(R.dimen.chooser_preview_image_height_tall) +                                ) +                            ) +                        } +                } +            } +        return composeView +    } +} diff --git a/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt new file mode 100644 index 00000000..58da5bc4 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.ClipData +import android.content.ClipDescription.compareMimeTypes +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.Intent.EXTRA_STREAM +import android.net.Uri + +/** Modifies target intent based on current payload selection. */ +class TargetIntentModifier<Item>( +    private val originalTargetIntent: Intent, +    private val getUri: Item.() -> Uri, +    private val getMimeType: Item.() -> String?, +) : (List<Item>) -> Intent { +    fun onSelectionChanged(selection: List<Item>): Intent { +        val uris = ArrayList<Uri>(selection.size) +        var targetMimeType: String? = null +        for (item in selection) { +            targetMimeType = updateMimeType(item.getMimeType(), targetMimeType) +            uris.add(item.getUri()) +        } +        val action = if (uris.size == 1) ACTION_SEND else ACTION_SEND_MULTIPLE +        return Intent(originalTargetIntent).apply { +            this.action = action +            this.type = targetMimeType +            if (action == ACTION_SEND) { +                putExtra(EXTRA_STREAM, uris[0]) +            } else { +                putParcelableArrayListExtra(EXTRA_STREAM, uris) +            } +            if (uris.isNotEmpty()) { +                clipData = +                    ClipData("", arrayOf(targetMimeType), ClipData.Item(uris[0])).also { +                        for (i in 1 until uris.size) { +                            it.addItem(ClipData.Item(uris[i])) +                        } +                    } +            } +        } +    } + +    private fun updateMimeType(itemMimeType: String?, unitedMimeType: String?): String { +        itemMimeType ?: return "*/*" +        unitedMimeType ?: return itemMimeType +        if (compareMimeTypes(itemMimeType, unitedMimeType)) return unitedMimeType +        val slashIdx = unitedMimeType.indexOf('/') +        if (slashIdx >= 0 && unitedMimeType.regionMatches(0, itemMimeType, 0, slashIdx + 1)) { +            return buildString { +                append(unitedMimeType.substring(0, slashIdx + 1)) +                append('*') +            } +        } +        return "*/*" +    } + +    override fun invoke(selection: List<Item>): Intent = onSelectionChanged(selection) +} diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index b0dc3c58..fbdc5853 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -30,6 +30,7 @@ import android.widget.TextView;  import androidx.annotation.Nullable; +import com.android.intentresolver.ContentTypeHint;  import com.android.intentresolver.R;  import com.android.intentresolver.widget.ActionRow; @@ -42,26 +43,33 @@ class TextContentPreviewUi extends ContentPreviewUi {      @Nullable      private final CharSequence mPreviewTitle;      @Nullable +    private final CharSequence mMetadata; +    @Nullable      private final Uri mPreviewThumbnail;      private final ImageLoader mImageLoader;      private final ChooserContentPreviewUi.ActionFactory mActionFactory;      private final HeadlineGenerator mHeadlineGenerator; +    private final ContentTypeHint mContentTypeHint;      TextContentPreviewUi(              CoroutineScope scope,              @Nullable CharSequence sharingText,              @Nullable CharSequence previewTitle, +            @Nullable CharSequence metadata,              @Nullable Uri previewThumbnail,              ChooserContentPreviewUi.ActionFactory actionFactory,              ImageLoader imageLoader, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            ContentTypeHint contentTypeHint) {          mScope = scope;          mSharingText = sharingText;          mPreviewTitle = previewTitle; +        mMetadata = metadata;          mPreviewThumbnail = previewThumbnail;          mImageLoader = imageLoader;          mActionFactory = actionFactory;          mHeadlineGenerator = headlineGenerator; +        mContentTypeHint = contentTypeHint;      }      @Override @@ -139,7 +147,11 @@ class TextContentPreviewUi extends ContentPreviewUi {              copyButton.setVisibility(View.GONE);          } -        displayHeadline(headlineViewParent, mHeadlineGenerator.getTextHeadline(mSharingText)); +        String headlineText = (mContentTypeHint == ContentTypeHint.ALBUM) +                ? mHeadlineGenerator.getAlbumHeadline() +                : mHeadlineGenerator.getTextHeadline(mSharingText); +        displayHeadline(headlineViewParent, headlineText); +        displayMetadata(headlineViewParent, mMetadata);          return contentPreviewLayout;      } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 8ddd5273..0974c79b 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -46,6 +46,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {      private final MimeTypeClassifier mTypeClassifier;      private final TransitionElementStatusCallback mTransitionElementStatusCallback;      private final HeadlineGenerator mHeadlineGenerator; +    @Nullable +    private final CharSequence mMetadata;      private final Flow<FileInfo> mFileInfoFlow;      private final int mItemCount;      @Nullable @@ -65,7 +67,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {              TransitionElementStatusCallback transitionElementStatusCallback,              Flow<FileInfo> fileInfoFlow,              int itemCount, -            HeadlineGenerator headlineGenerator) { +            HeadlineGenerator headlineGenerator, +            @Nullable CharSequence metadata) {          mShowEditAction = isSingleImage;          mIntentMimeType = intentMimeType;          mActionFactory = actionFactory; @@ -75,6 +78,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {          mFileInfoFlow = fileInfoFlow;          mItemCount = itemCount;          mHeadlineGenerator = headlineGenerator; +        mMetadata = metadata;          JavaFlowHelper.collectToList(scope, fileInfoFlow, this::setFiles);      } @@ -181,5 +185,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {          } else {              displayHeadline(layout, mHeadlineGenerator.getFilesHeadline(count));          } +        displayMetadata(layout, mMetadata);      }  } diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt new file mode 100644 index 00000000..41638b1f --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.ContentInterface +import android.database.Cursor +import android.media.MediaMetadata +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL +import android.provider.Downloads +import android.provider.OpenableColumns +import android.text.TextUtils +import android.util.Log +import com.android.intentresolver.measurements.runTracing + +internal fun ContentInterface.getTypeSafe(uri: Uri): String? = +    runTracing("getType") { +        try { +            getType(uri) +        } catch (e: SecurityException) { +            logProviderPermissionWarning(uri, "mime type") +            null +        } catch (t: Throwable) { +            Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) +            null +        } +    } + +internal fun ContentInterface.getStreamTypesSafe(uri: Uri): Array<String?> = +    runTracing("getStreamTypes") { +        try { +            getStreamTypes(uri, "*/*") ?: emptyArray() +        } catch (e: SecurityException) { +            logProviderPermissionWarning(uri, "stream types") +            emptyArray<String?>() +        } catch (t: Throwable) { +            Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: $uri", t) +            emptyArray<String?>() +        } +    } + +internal fun ContentInterface.querySafe(uri: Uri, columns: Array<String>): Cursor? = +    runTracing("query") { +        try { +            query(uri, columns, null, null) +        } catch (e: SecurityException) { +            logProviderPermissionWarning(uri, "metadata") +            null +        } catch (t: Throwable) { +            Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) +            null +        } +    } + +internal fun Cursor.readSupportsThumbnail(): Boolean = +    runCatching { +            val flagColIdx = columnNames.indexOf(DocumentsContract.Document.COLUMN_FLAGS) +            flagColIdx >= 0 && ((getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0) +        } +        .getOrDefault(false) + +internal fun Cursor.readPreviewUri(): Uri? = +    runCatching { +            columnNames +                .indexOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI) +                .takeIf { it >= 0 } +                ?.let { getString(it)?.let(Uri::parse) } +        } +        .getOrNull() + +internal fun Cursor.readTitle(): String = +    runCatching { +            var nameColIndex = -1 +            var titleColIndex = -1 +            // TODO: double-check why Cursor#getColumnInded didn't work +            columnNames.forEachIndexed { i, columnName -> +                when (columnName) { +                    OpenableColumns.DISPLAY_NAME -> nameColIndex = i +                    Downloads.Impl.COLUMN_TITLE -> titleColIndex = i +                } +            } + +            var title = "" +            if (nameColIndex >= 0) { +                title = getString(nameColIndex) ?: "" +            } +            if (TextUtils.isEmpty(title) && titleColIndex >= 0) { +                title = getString(titleColIndex) ?: "" +            } +            title +        } +        .getOrDefault("") + +private fun logProviderPermissionWarning(uri: Uri, dataName: String) { +    // The ContentResolver already logs the exception. Log something more informative. +    Log.w( +        ContentPreviewUi.TAG, +        "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" + +            " ensure that the sharesheet is given permission." +    ) +} diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt new file mode 100644 index 00000000..45515e25 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.content.ContentInterface +import android.media.MediaMetadata +import android.net.Uri +import android.provider.DocumentsContract + +class UriMetadataReader( +    private val contentResolver: ContentInterface, +    private val typeClassifier: MimeTypeClassifier, +) : (Uri) -> FileInfo { +    fun getMetadata(uri: Uri): FileInfo { +        val builder = FileInfo.Builder(uri) +        val mimeType = contentResolver.getTypeSafe(uri) +        builder.withMimeType(mimeType) +        if ( +            typeClassifier.isImageType(mimeType) || +                contentResolver.supportsImageType(uri) || +                contentResolver.supportsThumbnail(uri) +        ) { +            builder.withPreviewUri(uri) +            return builder.build() +        } +        val previewUri = contentResolver.readPreviewUri(uri) +        if (previewUri != null) { +            builder.withPreviewUri(previewUri) +        } +        return builder.build() +    } + +    override fun invoke(uri: Uri): FileInfo = getMetadata(uri) + +    private fun ContentInterface.supportsImageType(uri: Uri): Boolean = +        getStreamTypesSafe(uri).firstOrNull { typeClassifier.isImageType(it) } != null + +    private fun ContentInterface.supportsThumbnail(uri: Uri): Boolean = +        querySafe(uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS))?.use { cursor -> +            cursor.moveToFirst() && cursor.readSupportsThumbnail() +        } +            ?: false + +    private fun ContentInterface.readPreviewUri(uri: Uri): Uri? = +        querySafe(uri, arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))?.use { cursor -> +            if (cursor.moveToFirst()) { +                cursor.readPreviewUri() +            } else { +                null +            } +        } +} diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt new file mode 100644 index 00000000..87fb7618 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ComposeIconComposable.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.contentpreview.shareousel.ui.composable + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Resources +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import com.android.intentresolver.icon.AdaptiveIcon +import com.android.intentresolver.icon.BitmapIcon +import com.android.intentresolver.icon.ComposeIcon +import com.android.intentresolver.icon.ResourceIcon + +@Composable +fun Image(icon: ComposeIcon) { +    when (icon) { +        is AdaptiveIcon -> Image(icon.wrapped) +        is BitmapIcon -> Image(icon.bitmap.asImageBitmap(), contentDescription = null) +        is ResourceIcon -> { +            val localContext = LocalContext.current +            val wrappedContext: Context = +                object : ContextWrapper(localContext) { +                    override fun getResources(): Resources = icon.res +                } +            CompositionLocalProvider(LocalContext provides wrappedContext) { +                Image(painterResource(icon.resId), contentDescription = null) +            } +        } +    } +} diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt new file mode 100644 index 00000000..dc96e3c1 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselCardComposable.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.contentpreview.shareousel.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.android.intentresolver.R + +@Composable +fun ShareouselCard( +    image: @Composable () -> Unit, +    selected: Boolean, +    modifier: Modifier = Modifier, +) { +    Box(modifier) { +        image() +        val topButtonPadding = 12.dp +        Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) { +            SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart)) +            AnimationIcon(modifier = Modifier.align(Alignment.TopEnd)) +        } +    } +} + +@Composable +private fun AnimationIcon(modifier: Modifier = Modifier) { +    Icon( +        painterResource(id = R.drawable.ic_play_circle_filled_24px), +        "animating", +        tint = Color.White, +        modifier = Modifier.size(20.dp).then(modifier) +    ) +} + +@Composable +private fun SelectionIcon(selected: Boolean, modifier: Modifier = Modifier) { +    if (selected) { +        val bgColor = MaterialTheme.colorScheme.primary +        Icon( +            painter = painterResource(id = R.drawable.checkbox), +            tint = Color.White, +            contentDescription = "selected", +            modifier = +                Modifier.shadow( +                        elevation = 50.dp, +                        spotColor = Color(0x40000000), +                        ambientColor = Color(0x40000000) +                    ) +                    .size(20.dp) +                    .drawBehind { +                        drawCircle(color = bgColor, radius = (this.size.width / 2f) - 1f) +                    } +                    .then(modifier) +        ) +    } else { +        Box( +            modifier = +                Modifier.shadow( +                        elevation = 50.dp, +                        spotColor = Color(0x40000000), +                        ambientColor = Color(0x40000000), +                    ) +                    .border(width = 2.dp, color = Color(0xFFFFFFFF), shape = CircleShape) +                    .clip(CircleShape) +                    .size(20.dp) +                    .background(color = Color(0x7DC4C4C4)) +                    .then(modifier) +        ) +    } +} diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt new file mode 100644 index 00000000..5cf35297 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.contentpreview.shareousel.ui.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AssistChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.intentresolver.R +import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselImageViewModel +import com.android.intentresolver.contentpreview.shareousel.ui.viewmodel.ShareouselViewModel + +@Composable +fun Shareousel(viewModel: ShareouselViewModel) { +    val centerIdx = viewModel.centerIndex.value +    val carouselState = rememberLazyListState(initialFirstVisibleItemIndex = centerIdx) +    val previewKeys by viewModel.previewKeys.collectAsStateWithLifecycle() +    Column { +        // TODO: item needs to be centered, check out ScalingLazyColumn impl or see if +        //  HorizontalPager works for our use-case +        LazyRow( +            state = carouselState, +            horizontalArrangement = Arrangement.spacedBy(4.dp), +            modifier = +                Modifier.fillMaxWidth() +                    .height(dimensionResource(R.dimen.chooser_preview_image_height_tall)) +        ) { +            items(previewKeys, key = viewModel.previewRowKey) { key -> +                ShareouselCard(viewModel.previewForKey(key)) +            } +        } +        Spacer(modifier = Modifier.height(8.dp)) + +        val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList()) +        LazyRow( +            horizontalArrangement = Arrangement.spacedBy(4.dp), +        ) { +            items(actions) { actionViewModel -> +                ShareouselAction( +                    label = actionViewModel.label, +                    onClick = actionViewModel.onClick, +                ) { +                    actionViewModel.icon?.let { Image(it) } +                } +            } +        } +    } +} + +private const val MIN_ASPECT_RATIO = 0.4f +private const val MAX_ASPECT_RATIO = 2.5f + +@Composable +private fun ShareouselCard(viewModel: ShareouselImageViewModel) { +    val bitmap by viewModel.bitmap.collectAsStateWithLifecycle(initialValue = null) +    val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false) +    val contentDescription by +        viewModel.contentDescription.collectAsStateWithLifecycle(initialValue = null) +    val borderColor = MaterialTheme.colorScheme.primary + +    ShareouselCard( +        image = { +            bitmap?.let { bitmap -> +                val aspectRatio = +                    (bitmap.width.toFloat() / bitmap.height.toFloat()) +                        // TODO: max ratio is actually equal to the viewport ratio +                        .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) +                Image( +                    bitmap = bitmap.asImageBitmap(), +                    contentDescription = contentDescription, +                    contentScale = ContentScale.Crop, +                    modifier = Modifier.aspectRatio(aspectRatio), +                ) +            } +                ?: run { +                    // TODO: look at ScrollableImagePreviewView.setLoading() +                    Box(modifier = Modifier.aspectRatio(2f / 5f)) +                } +        }, +        selected = selected, +        modifier = +            Modifier.thenIf(selected) { +                    Modifier.border( +                        width = 4.dp, +                        color = borderColor, +                        shape = RoundedCornerShape(size = 12.dp) +                    ) +                } +                .clip(RoundedCornerShape(size = 12.dp)) +                .clickable { viewModel.setSelected(!selected) }, +    ) +} + +@Composable +private fun ShareouselAction( +    label: String, +    onClick: () -> Unit, +    modifier: Modifier = Modifier, +    leadingIcon: (@Composable () -> Unit)? = null, +) { +    AssistChip( +        onClick = onClick, +        label = { Text(label) }, +        leadingIcon = leadingIcon, +        modifier = modifier +    ) +} + +inline fun Modifier.thenIf(condition: Boolean, crossinline factory: () -> Modifier): Modifier = +    if (condition) this.then(factory()) else this diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt new file mode 100644 index 00000000..18ee2539 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/viewmodel/ShareouselViewModel.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.contentpreview.shareousel.ui.viewmodel + +import android.graphics.Bitmap +import androidx.core.graphics.drawable.toBitmap +import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory +import com.android.intentresolver.contentpreview.ImageLoader +import com.android.intentresolver.contentpreview.MutableActionFactory +import com.android.intentresolver.contentpreview.PayloadToggleInteractor +import com.android.intentresolver.icon.BitmapIcon +import com.android.intentresolver.icon.ComposeIcon +import com.android.intentresolver.widget.ActionRow.Action +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +data class ShareouselViewModel( +    val headline: Flow<String>, +    val previewKeys: StateFlow<List<Any>>, +    val actions: Flow<List<ActionChipViewModel>>, +    val centerIndex: StateFlow<Int>, +    val previewForKey: (key: Any) -> ShareouselImageViewModel, +    val previewRowKey: (Any) -> Any +) + +data class ActionChipViewModel(val label: String, val icon: ComposeIcon?, val onClick: () -> Unit) + +data class ShareouselImageViewModel( +    val bitmap: Flow<Bitmap?>, +    val contentDescription: Flow<String>, +    val isSelected: Flow<Boolean>, +    val setSelected: (Boolean) -> Unit, +) + +suspend fun PayloadToggleInteractor.toShareouselViewModel( +    imageLoader: ImageLoader, +    actionFactory: ActionFactory, +    scope: CoroutineScope, +): ShareouselViewModel { +    return ShareouselViewModel( +        headline = MutableStateFlow("Shareousel"), +        previewKeys = previewKeys.stateIn(scope), +        actions = +            if (actionFactory is MutableActionFactory) { +                actionFactory.customActionsFlow.map { actions -> +                    actions.map { it.toActionChipViewModel() } +                } +            } else { +                flow { +                    emit(actionFactory.createCustomActions().map { it.toActionChipViewModel() }) +                } +            }, +        centerIndex = targetPosition.stateIn(scope), +        previewForKey = { key -> +            val previewInteractor = previewInteractor(key) +            ShareouselImageViewModel( +                bitmap = previewInteractor.previewUri.map { uri -> uri?.let { imageLoader(uri) } }, +                contentDescription = MutableStateFlow(""), +                isSelected = previewInteractor.selected, +                setSelected = { isSelected -> previewInteractor.setSelected(isSelected) }, +            ) +        }, +        previewRowKey = { getKey(it) }, +    ) +} + +private fun Action.toActionChipViewModel() = +    ActionChipViewModel( +        label?.toString() ?: "", +        icon?.let { BitmapIcon(it.toBitmap()) }, +        onClick = { onClicked.run() } +    ) diff --git a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java index 2653c560..5f10cf32 100644 --- a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -29,9 +29,9 @@ import android.stats.devicepolicy.nano.DevicePolicyEnums;  import androidx.annotation.NonNull;  import androidx.annotation.Nullable; +import com.android.intentresolver.R;  import com.android.intentresolver.ResolvedComponentInfo;  import com.android.intentresolver.ResolverListAdapter; -import com.android.internal.R;  import java.util.List; diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java index 51d4e677..036b686b 100644 --- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java +++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java @@ -85,19 +85,11 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.           * long-pressed.           */          void onTargetLongPressed(int itemIndex); - -        /** -         * Notify the client that the provided {@code View} should be configured as the new -         * "profile view" button. Callers should attach their own click listeners to implement -         * behaviors on this view. -         */ -        void updateProfileViewButton(View newButtonFromProfileRow);      }      private static final int VIEW_TYPE_DIRECT_SHARE = 0;      private static final int VIEW_TYPE_NORMAL = 1;      private static final int VIEW_TYPE_CONTENT_PREVIEW = 2; -    private static final int VIEW_TYPE_PROFILE = 3;      private static final int VIEW_TYPE_AZ_LABEL = 4;      private static final int VIEW_TYPE_CALLER_AND_RANK = 5;      private static final int VIEW_TYPE_FOOTER = 6; @@ -170,7 +162,14 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      }      public void setFooterHeight(int height) { -        mFooterHeight = height; +        if (mFooterHeight != height) { +            mFooterHeight = height; +            if (mFeatureFlags.fixTargetListFooter()) { +                // we always have at least one view, the footer, see getItemCount() and +                // getFooterRowCount() +                notifyItemChanged(getItemCount() - 1); +            } +        }      }      /** @@ -201,7 +200,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      public int getRowCount() {          return (int) (                  getSystemRowCount() -                        + getProfileRowCount()                          + getServiceTargetRowCount()                          + getCallerAndRankedTargetRowCount()                          + getAzLabelRowCount() @@ -234,13 +232,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          return 1;      } -    public int getProfileRowCount() { -        if (mChooserActivityDelegate.shouldShowTabs()) { -            return 0; -        } -        return mChooserListAdapter.getOtherProfile() == null ? 0 : 1; -    } -      public int getFooterRowCount() {          return 1;      } @@ -272,7 +263,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          }          return getSystemRowCount() -                + getProfileRowCount()                  + getServiceTargetRowCount()                  + getCallerAndRankedTargetRowCount();      } @@ -280,7 +270,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      @Override      public int getItemCount() {          return getSystemRowCount() -                + getProfileRowCount()                  + getServiceTargetRowCount()                  + getCallerAndRankedTargetRowCount()                  + getAzLabelRowCount() @@ -298,12 +287,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.                          viewType,                          null,                          null); -            case VIEW_TYPE_PROFILE: -                return new ItemViewHolder( -                        createProfileView(parent), -                        viewType, -                        null, -                        null);              case VIEW_TYPE_AZ_LABEL:                  return new ItemViewHolder(                          createAzLabelView(parent), @@ -379,9 +362,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          int countSum = (count = getSystemRowCount());          if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW; -        countSum += (count = getProfileRowCount()); -        if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE; -          countSum += (count = getServiceTargetRowCount());          if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE; @@ -400,12 +380,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          return mChooserListAdapter.getPositionTargetType(getListPosition(position));      } -    private View createProfileView(ViewGroup parent) { -        View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false); -        mChooserActivityDelegate.updateProfileViewButton(profileRow); -        return profileRow; -    } -      private View createAzLabelView(ViewGroup parent) {          return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);      } @@ -583,7 +557,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      }      int getListPosition(int position) { -        position -= getSystemRowCount() + getProfileRowCount(); +        position -= getSystemRowCount();          final int serviceCount = mChooserListAdapter.getServiceTargetCount();          final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow); diff --git a/java/src/com/android/intentresolver/icon/ComposeIcon.kt b/java/src/com/android/intentresolver/icon/ComposeIcon.kt new file mode 100644 index 00000000..dbea1e55 --- /dev/null +++ b/java/src/com/android/intentresolver/icon/ComposeIcon.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.icon + +import android.content.ContentResolver +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.Icon +import java.io.File +import java.io.FileInputStream + +sealed interface ComposeIcon + +data class BitmapIcon(val bitmap: Bitmap) : ComposeIcon + +data class ResourceIcon(val resId: Int, val res: Resources) : ComposeIcon + +@JvmInline value class AdaptiveIcon(val wrapped: ComposeIcon) : ComposeIcon + +fun Icon.toComposeIcon(pm: PackageManager, resolver: ContentResolver): ComposeIcon? { +    return when (type) { +        Icon.TYPE_BITMAP -> BitmapIcon(bitmap) +        Icon.TYPE_RESOURCE -> pm.resourcesForPackage(resPackage)?.let { ResourceIcon(resId, it) } +        Icon.TYPE_DATA -> +            BitmapIcon(BitmapFactory.decodeByteArray(dataBytes, dataOffset, dataLength)) +        Icon.TYPE_URI -> uriIcon(resolver) +        Icon.TYPE_ADAPTIVE_BITMAP -> AdaptiveIcon(BitmapIcon(bitmap)) +        Icon.TYPE_URI_ADAPTIVE_BITMAP -> uriIcon(resolver)?.let { AdaptiveIcon(it) } +        else -> error("unexpected icon type: $type") +    } +} + +fun Icon.toComposeIcon(resources: Resources?, resolver: ContentResolver): ComposeIcon? { +    return when (type) { +        Icon.TYPE_BITMAP -> BitmapIcon(bitmap) +        Icon.TYPE_RESOURCE -> resources?.let { ResourceIcon(resId, resources) } +        Icon.TYPE_DATA -> +            BitmapIcon(BitmapFactory.decodeByteArray(dataBytes, dataOffset, dataLength)) +        Icon.TYPE_URI -> uriIcon(resolver) +        Icon.TYPE_ADAPTIVE_BITMAP -> AdaptiveIcon(BitmapIcon(bitmap)) +        Icon.TYPE_URI_ADAPTIVE_BITMAP -> uriIcon(resolver)?.let { AdaptiveIcon(it) } +        else -> error("unexpected icon type: $type") +    } +} + +// TODO: this is probably constant and doesn't need to be re-queried for each icon +fun PackageManager.resourcesForPackage(pkgName: String): Resources? { +    return if (pkgName == "android") { +        Resources.getSystem() +    } else { +        runCatching { +                this@resourcesForPackage.getApplicationInfo( +                    pkgName, +                    PackageManager.MATCH_UNINSTALLED_PACKAGES or +                        PackageManager.GET_SHARED_LIBRARY_FILES +                ) +            } +            .getOrNull() +            ?.let { ai -> getResourcesForApplication(ai) } +    } +} + +private fun Icon.uriIcon(resolver: ContentResolver): BitmapIcon? { +    return runCatching { +            when (uri.scheme) { +                ContentResolver.SCHEME_CONTENT, +                ContentResolver.SCHEME_FILE -> resolver.openInputStream(uri) +                else -> FileInputStream(File(uriString)) +            } +        } +        .getOrNull() +        ?.let { inStream -> BitmapIcon(BitmapFactory.decodeStream(inStream)) } +} diff --git a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt index 05cf2104..0f9a18c1 100644 --- a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt +++ b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt @@ -1,15 +1,25 @@  package com.android.intentresolver.inject -import com.android.intentresolver.FeatureFlags -import com.android.intentresolver.FeatureFlagsImpl +import android.service.chooser.FeatureFlagsImpl as ChooserServiceFlagsImpl +import com.android.intentresolver.FeatureFlagsImpl as IntentResolverFlagsImpl  import dagger.Module  import dagger.Provides  import dagger.hilt.InstallIn  import dagger.hilt.components.SingletonComponent +typealias IntentResolverFlags = com.android.intentresolver.FeatureFlags + +typealias FakeIntentResolverFlags = com.android.intentresolver.FakeFeatureFlagsImpl + +typealias ChooserServiceFlags = android.service.chooser.FeatureFlags + +typealias FakeChooserServiceFlags = android.service.chooser.FakeFeatureFlagsImpl +  @Module  @InstallIn(SingletonComponent::class)  object FeatureFlagsModule { -    @Provides fun featureFlags(): FeatureFlags = FeatureFlagsImpl() +    @Provides fun intentResolverFlags(): IntentResolverFlags = IntentResolverFlagsImpl() + +    @Provides fun chooserServiceFlags(): ChooserServiceFlags = ChooserServiceFlagsImpl()  } diff --git a/java/src/com/android/intentresolver/inject/FrameworkModule.kt b/java/src/com/android/intentresolver/inject/FrameworkModule.kt deleted file mode 100644 index 2f6cc6a0..00000000 --- a/java/src/com/android/intentresolver/inject/FrameworkModule.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.inject - -import android.app.ActivityManager -import android.app.admin.DevicePolicyManager -import android.content.ClipboardManager -import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.ShortcutManager -import android.os.UserManager -import android.view.WindowManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent - -private fun <T> Context.requireSystemService(serviceClass: Class<T>): T { -    return checkNotNull(getSystemService(serviceClass)) -} - -@Module -@InstallIn(SingletonComponent::class) -object FrameworkModule { - -    @Provides -    fun contentResolver(@ApplicationContext ctx: Context) = -        requireNotNull(ctx.contentResolver) { "ContentResolver is expected but missing" } - -    @Provides -    fun activityManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(ActivityManager::class.java) - -    @Provides -    fun clipboardManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(ClipboardManager::class.java) - -    @Provides -    fun devicePolicyManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(DevicePolicyManager::class.java) - -    @Provides -    fun launcherApps(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(LauncherApps::class.java) - -    @Provides -    fun packageManager(@ApplicationContext ctx: Context) = -        requireNotNull(ctx.packageManager) { "PackageManager is expected but missing" } - -    @Provides -    fun shortcutManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(ShortcutManager::class.java) - -    @Provides -    fun userManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(UserManager::class.java) - -    @Provides -    fun windowManager(@ApplicationContext ctx: Context) = -        ctx.requireSystemService(WindowManager::class.java) -} diff --git a/java/src/com/android/intentresolver/inject/SystemServices.kt b/java/src/com/android/intentresolver/inject/SystemServices.kt new file mode 100644 index 00000000..32894d43 --- /dev/null +++ b/java/src/com/android/intentresolver/inject/SystemServices.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.inject + +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.ClipboardManager +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.ShortcutManager +import android.os.UserManager +import android.view.WindowManager +import androidx.core.content.getSystemService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +inline fun <reified T> Context.requireSystemService(): T { +    return checkNotNull(getSystemService()) +} + +@Module +@InstallIn(SingletonComponent::class) +class ActivityManagerModule { +    @Provides +    fun activityManager(@ApplicationContext ctx: Context): ActivityManager = +        ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class ClipboardManagerModule { +    @Provides +    fun clipboardManager(@ApplicationContext ctx: Context): ClipboardManager = +        ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class ContentResolverModule { +    @Provides +    fun contentResolver(@ApplicationContext ctx: Context) = requireNotNull(ctx.contentResolver) +} + +@Module +@InstallIn(SingletonComponent::class) +class DevicePolicyManagerModule { +    @Provides +    fun devicePolicyManager(@ApplicationContext ctx: Context): DevicePolicyManager = +        ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class LauncherAppsModule { +    @Provides +    fun launcherApps(@ApplicationContext ctx: Context): LauncherApps = ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class PackageManagerModule { +    @Provides +    fun packageManager(@ApplicationContext ctx: Context) = requireNotNull(ctx.packageManager) +} + +@Module +@InstallIn(SingletonComponent::class) +class ShortcutManagerModule { +    @Provides +    fun shortcutManager(@ApplicationContext ctx: Context): ShortcutManager = +        ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class UserManagerModule { +    @Provides +    fun userManager(@ApplicationContext ctx: Context): UserManager = ctx.requireSystemService() +} + +@Module +@InstallIn(SingletonComponent::class) +class WindowManagerModule { +    @Provides +    fun windowManager(@ApplicationContext ctx: Context): WindowManager = ctx.requireSystemService() +} diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java index 0651d26c..c6de3260 100644 --- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java @@ -107,16 +107,20 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp                      .setClassName(target.name.getClassName())                      .build());          } -        mAppPredictor.sortTargets( -                appTargets, -                Executors.newSingleThreadExecutor(), -                new ScopedAppTargetListCallback( -                        mContext, -                        sortedAppTargets -> { -                            onAppTargetsSorted(targets, sortedAppTargets); -                            return kotlin.Unit.INSTANCE; -                        }).toConsumer() -        ); +        try { +            mAppPredictor.sortTargets( +                    appTargets, +                    Executors.newSingleThreadExecutor(), +                    new ScopedAppTargetListCallback( +                            mContext, +                            sortedAppTargets -> { +                                onAppTargetsSorted(targets, sortedAppTargets); +                                return kotlin.Unit.INSTANCE; +                            }).toConsumer() +            ); +        } catch (IllegalStateException e) { +            Log.w(TAG, "Couldn't sort targets with AppPredictionService", e); +        }      }      private void onAppTargetsSorted( @@ -292,8 +296,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp                      new AppTarget.Builder(targetId, targetComponent.getPackageName(), mUser)                              .setClassName(targetComponent.getClassName())                              .build(); -            mAppPredictor.notifyAppTargetEvent( -                    new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build()); +            try { +                mAppPredictor.notifyAppTargetEvent( +                        new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build()); +            } catch (IllegalStateException e) { +                Log.w(TAG, "Couldn't send feedback to AppPredictionService", e); +            }          }      }  } diff --git a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt index 82f40b91..e544e064 100644 --- a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt +++ b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt @@ -41,16 +41,14 @@ private const val SHARED_TEXT_KEY = "shared_text"  class AppPredictorFactory(      private val context: Context,      private val sharedText: String?, -    private val targetIntentFilter: IntentFilter? +    private val targetIntentFilter: IntentFilter?, +    private val appPredictionAvailable: Boolean,  ) { -    private val mIsComponentAvailable = -        context.packageManager.appPredictionServicePackageName != null -      /**       * Creates an AppPredictor instance for a profile or `null` if app predictor is not available.       */      fun create(userHandle: UserHandle): AppPredictor? { -        if (!mIsComponentAvailable) return null +        if (!appPredictionAvailable) return null          val contextAsUser = context.createContextAsUser(userHandle, 0 /* flags */)          val extras = Bundle().apply {              putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, targetIntentFilter) diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt index a8b59fb0..08230d90 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -186,6 +186,11 @@ constructor(          // Default to just querying ShortcutManager if AppPredictor not present.          if (targetIntentFilter == null) {              Log.d(TAG, "skip querying ShortcutManager for $userHandle") +            sendShareShortcutInfoList( +                emptyList(), +                isFromAppPredictor = false, +                appPredictorTargets = null +            )              return          }          Log.d(TAG, "query ShortcutManager for user $userHandle") diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt index c81bed09..62ace0da 100644 --- a/java/src/com/android/intentresolver/v2/ActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt @@ -1,18 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */  package com.android.intentresolver.v2 -import android.app.admin.DevicePolicyManager -import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL -import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK -import android.content.Intent  import android.os.UserHandle  import android.os.UserManager  import android.util.Log  import androidx.activity.ComponentActivity  import androidx.core.content.getSystemService  import com.android.intentresolver.AnnotatedUserHandles -import com.android.intentresolver.R  import com.android.intentresolver.WorkProfileAvailabilityManager -import com.android.intentresolver.icons.TargetDataLoader  /**   * Logic for IntentResolver Activities. Anything that is not the same across activities (including @@ -20,38 +29,7 @@ import com.android.intentresolver.icons.TargetDataLoader   * activity, including test activities, but all implementations should delegate to a   * CommonActivityLogic implementation.   */ -interface ActivityLogic : CommonActivityLogic { -    /** The intent for the target. This will always come before additional targets, if any. */ -    val targetIntent: Intent -    /** Whether the intent is for home. */ -    val resolvingHome: Boolean -    /** Custom title to display. */ -    val title: CharSequence? -    /** Resource ID for the title to display when there is no custom title. */ -    val defaultTitleResId: Int -    /** Intents received to be processed. */ -    val initialIntents: List<Intent>? -    /** Whether or not this activity supports choosing a default handler for the intent. */ -    val supportsAlwaysUseOption: Boolean -    /** Fetches display info for processed candidates. */ -    val targetDataLoader: TargetDataLoader -    /** The theme to use. */ -    val themeResId: Int -    /** -     * Message showing that intent is forwarded from managed profile to owner or other way around. -     */ -    val profileSwitchMessage: String? -    /** The intents for potential actual targets. [targetIntent] must be first. */ -    val payloadIntents: List<Intent> - -    /** -     * Called after Activity superclass creation, but before any other onCreate logic is performed. -     */ -    fun preInitialization() - -    /** Sets [profileSwitchMessage] to null */ -    fun clearProfileSwitchMessage() -} +interface ActivityLogic : CommonActivityLogic  /**   * Logic that is common to all IntentResolver activities. Anything that is the same across @@ -60,21 +38,15 @@ interface ActivityLogic : CommonActivityLogic {  interface CommonActivityLogic {      /** The tag to use when logging. */      val tag: String +      /** A reference to the activity owning, and used by, this logic. */      val activity: ComponentActivity -    /** The name of the referring package. */ -    val referrerPackageName: String? -    /** User manager system service. */ -    val userManager: UserManager -    /** Device policy manager system service. */ -    val devicePolicyManager: DevicePolicyManager +      /** Current [UserHandle]s retrievable by type. */      val annotatedUserHandles: AnnotatedUserHandles? +      /** Monitors for changes to work profile availability. */      val workProfileAvailabilityManager: WorkProfileAvailabilityManager - -    /** Returns display message indicating intent forwarding or null if not intent forwarding. */ -    fun forwardMessageFor(intent: Intent): String?  }  /** @@ -84,73 +56,24 @@ interface CommonActivityLogic {   */  class CommonActivityLogicImpl(      override val tag: String, -    activityProvider: () -> ComponentActivity, +    override val activity: ComponentActivity,      onWorkProfileStatusUpdated: () -> Unit,  ) : CommonActivityLogic { -    override val activity: ComponentActivity by lazy { activityProvider() } - -    override val referrerPackageName: String? by lazy { -        activity.referrer.let { -            if (ANDROID_APP_URI_SCHEME == it?.scheme) { -                it.host -            } else { -                null -            } -        } -    } +    private val userManager: UserManager = activity.getSystemService()!! -    override val userManager: UserManager by lazy { activity.getSystemService()!! } - -    override val devicePolicyManager: DevicePolicyManager by lazy { activity.getSystemService()!! } - -    override val annotatedUserHandles: AnnotatedUserHandles? by lazy { +    override val annotatedUserHandles: AnnotatedUserHandles? =          try {              AnnotatedUserHandles.forShareActivity(activity)          } catch (e: SecurityException) {              Log.e(tag, "Request from UID without necessary permissions", e)              null          } -    } -    override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy { +    override val workProfileAvailabilityManager =          WorkProfileAvailabilityManager(              userManager,              annotatedUserHandles?.workProfileUserHandle,              onWorkProfileStatusUpdated,          ) -    } - -    private val forwardToPersonalMessage: String? by lazy { -        devicePolicyManager.resources.getString(FORWARD_INTENT_TO_PERSONAL) { -            activity.getString(R.string.forward_intent_to_owner) -        } -    } - -    private val forwardToWorkMessage: String? by lazy { -        devicePolicyManager.resources.getString(FORWARD_INTENT_TO_WORK) { -            activity.getString(R.string.forward_intent_to_work) -        } -    } - -    override fun forwardMessageFor(intent: Intent): String? { -        val contentUserHint = intent.contentUserHint -        if ( -            contentUserHint != UserHandle.USER_CURRENT && contentUserHint != UserHandle.myUserId() -        ) { -            val originUserInfo = userManager.getUserInfo(contentUserHint) -            val originIsManaged = originUserInfo?.isManagedProfile ?: false -            val targetIsManaged = userManager.isManagedProfile -            return when { -                originIsManaged && !targetIsManaged -> forwardToPersonalMessage -                !originIsManaged && targetIsManaged -> forwardToWorkMessage -                else -> null -            } -        } -        return null -    } - -    companion object { -        private const val ANDROID_APP_URI_SCHEME = "android-app" -    }  } diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java index db840387..9077a18d 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java @@ -40,6 +40,8 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;  import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.v2.ui.ShareResultSender; +import com.android.intentresolver.v2.ui.model.ShareAction;  import com.android.intentresolver.widget.ActionRow;  import com.android.internal.annotations.VisibleForTesting; @@ -97,12 +99,12 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      private final Context mContext; -    @Nullable -    private final Runnable mCopyButtonRunnable; -    private final Runnable mEditButtonRunnable; +    @Nullable private Runnable mCopyButtonRunnable; +    private Runnable mEditButtonRunnable;      private final ImmutableList<ChooserAction> mCustomActions; -    private final @Nullable ChooserAction mModifyShareAction; +    @Nullable private final ChooserAction mModifyShareAction;      private final Consumer<Boolean> mExcludeSharedTextAction; +    @Nullable private final ShareResultSender mShareResultSender;      private final Consumer</* @Nullable */ Integer> mFinishCallback;      private final EventLog mLog; @@ -122,17 +124,19 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              Intent targetIntent,              String referrerPackageName,              List<ChooserAction> chooserActions, -            ChooserAction modifyShareAction, +            @Nullable ChooserAction modifyShareAction,              Optional<ComponentName> imageEditor,              EventLog log,              Consumer<Boolean> onUpdateSharedTextIsExcluded,              Callable</* @Nullable */ View> firstVisibleImageQuery,              ActionActivityStarter activityStarter, -            Consumer</* @Nullable */ Integer> finishCallback) { +            @Nullable ShareResultSender shareResultSender, +            Consumer</* @Nullable */ Integer> finishCallback, +            ClipboardManager clipboardManager) {          this(                  context,                  makeCopyButtonRunnable( -                        context, +                        clipboardManager,                          targetIntent,                          referrerPackageName,                          finishCallback, @@ -149,7 +153,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                  modifyShareAction,                  onUpdateSharedTextIsExcluded,                  log, +                shareResultSender,                  finishCallback); +      }      @VisibleForTesting @@ -161,6 +167,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              @Nullable ChooserAction modifyShareAction,              Consumer<Boolean> onUpdateSharedTextIsExcluded,              EventLog log, +            @Nullable ShareResultSender shareResultSender,              Consumer</* @Nullable */ Integer> finishCallback) {          mContext = context;          mCopyButtonRunnable = copyButtonRunnable; @@ -169,7 +176,21 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio          mModifyShareAction = modifyShareAction;          mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;          mLog = log; +        mShareResultSender = shareResultSender;          mFinishCallback = finishCallback; + +        if (mShareResultSender != null) { +            mEditButtonRunnable = () -> { +                mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT); +                editButtonRunnable.run(); +            }; +            if (mCopyButtonRunnable != null) { +                mCopyButtonRunnable = () -> { +                    mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY); +                    copyButtonRunnable.run(); +                }; +            } +        }      }      @Override @@ -191,13 +212,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio          for (int i = 0; i < mCustomActions.size(); i++) {              final int position = i;              ActionRow.Action actionRow = createCustomAction( -                    mContext, -                    mCustomActions.get(i), -                    mFinishCallback, -                    () -> { -                        mLog.logCustomActionSelected(position); -                    } -            ); +                    mCustomActions.get(i), () -> logCustomAction(position));              if (actionRow != null) {                  actions.add(actionRow);              } @@ -211,13 +226,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      @Override      @Nullable      public ActionRow.Action getModifyShareAction() { -        return createCustomAction( -                mContext, -                mModifyShareAction, -                mFinishCallback, -                () -> { -                    mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE); -                }); +        return createCustomAction(mModifyShareAction, this::logModifyShareAction);      }      /** @@ -236,7 +245,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      @Nullable      private static Runnable makeCopyButtonRunnable( -            Context context, +            ClipboardManager clipboardManager,              Intent targetIntent,              String referrerPackageName,              Consumer<Integer> finishCallback, @@ -252,8 +261,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              return null;          }          return () -> { -            ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService( -                    Context.CLIPBOARD_SERVICE);              clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);              log.logActionSelected(EventLog.SELECTION_TYPE_COPY); @@ -353,15 +360,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      }      @Nullable -    private static ActionRow.Action createCustomAction( -            Context context, -            ChooserAction action, -            Consumer<Integer> finishCallback, -            Runnable loggingRunnable) { -        if (action == null || action.getAction() == null) { +    ActionRow.Action createCustomAction(@Nullable ChooserAction action, Runnable loggingRunnable) { +        if (action == null) {              return null;          } -        Drawable icon = action.getIcon().loadDrawable(context); +        Drawable icon = action.getIcon().loadDrawable(mContext);          if (icon == null && TextUtils.isEmpty(action.getLabel())) {              return null;          } @@ -378,7 +381,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                                  null,                                  null,                                  ActivityOptions.makeCustomAnimation( -                                                context, +                                                mContext,                                                  R.anim.slide_in_right,                                                  R.anim.slide_out_left)                                          .toBundle()); @@ -388,8 +391,19 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                      if (loggingRunnable != null) {                          loggingRunnable.run();                      } -                    finishCallback.accept(Activity.RESULT_OK); +                    if (mShareResultSender != null) { +                        mShareResultSender.onActionSelected(ShareAction.APPLICATION_DEFINED); +                    } +                    mFinishCallback.accept(Activity.RESULT_OK);                  }          );      } + +    void logCustomAction(int position) { +        mLog.logCustomActionSelected(position); +    } + +    private void logModifyShareAction() { +        mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE); +    }  } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 70812642..7b5ff541 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -1,5 +1,5 @@  /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License. @@ -16,27 +16,39 @@  package com.android.intentresolver.v2; +import static android.app.VoiceInteractor.PickOptionRequest.Option;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;  import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;  import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;  import static androidx.lifecycle.LifecycleKt.getCoroutineScope; +import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION; +import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs; +import static com.android.intentresolver.v2.ui.model.ActivityModel.ACTIVITY_MODEL_KEY; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;  import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; +import static java.util.Collections.emptyList;  import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; -import android.app.Activity;  import android.app.ActivityManager;  import android.app.ActivityOptions; +import android.app.ActivityThread; +import android.app.VoiceInteractor; +import android.app.admin.DevicePolicyEventLogger;  import android.app.prediction.AppPredictor;  import android.app.prediction.AppTarget;  import android.app.prediction.AppTargetEvent;  import android.app.prediction.AppTargetId; +import android.content.ClipboardManager;  import android.content.ComponentName;  import android.content.ContentResolver;  import android.content.Context; @@ -45,32 +57,48 @@ import android.content.IntentFilter;  import android.content.IntentSender;  import android.content.SharedPreferences;  import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo;  import android.content.pm.PackageManager;  import android.content.pm.ResolveInfo;  import android.content.pm.ShortcutInfo; +import android.content.pm.UserInfo;  import android.content.res.Configuration;  import android.database.Cursor;  import android.graphics.Insets;  import android.net.Uri; +import android.os.Build;  import android.os.Bundle; +import android.os.StrictMode;  import android.os.SystemClock; +import android.os.Trace;  import android.os.UserHandle;  import android.os.UserManager;  import android.service.chooser.ChooserTarget; +import android.stats.devicepolicy.DevicePolicyEnums; +import android.text.TextUtils;  import android.util.Log;  import android.util.Slog; -import android.util.SparseArray; +import android.view.Gravity; +import android.view.LayoutInflater;  import android.view.View;  import android.view.ViewGroup;  import android.view.ViewGroup.LayoutParams;  import android.view.ViewTreeObserver; +import android.view.Window;  import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TabHost;  import android.widget.TextView; +import android.widget.Toast;  import androidx.annotation.MainThread;  import androidx.annotation.NonNull;  import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity;  import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.viewmodel.CreationExtras;  import androidx.recyclerview.widget.GridLayoutManager;  import androidx.recyclerview.widget.RecyclerView;  import androidx.viewpager.widget.ViewPager; @@ -79,23 +107,28 @@ import com.android.intentresolver.AnnotatedUserHandles;  import com.android.intentresolver.ChooserGridLayoutManager;  import com.android.intentresolver.ChooserListAdapter;  import com.android.intentresolver.ChooserRefinementManager; -import com.android.intentresolver.ChooserRequestParameters;  import com.android.intentresolver.ChooserStackedAppDialogFragment;  import com.android.intentresolver.ChooserTargetActionsDialogFragment;  import com.android.intentresolver.EnterTransitionAnimationDelegate;  import com.android.intentresolver.FeatureFlags;  import com.android.intentresolver.IntentForwarderActivity; +import com.android.intentresolver.PackagesChangedListener;  import com.android.intentresolver.R;  import com.android.intentresolver.ResolverListAdapter;  import com.android.intentresolver.ResolverListController;  import com.android.intentresolver.ResolverViewPager; +import com.android.intentresolver.StartsSelectedItem; +import com.android.intentresolver.WorkProfileAvailabilityManager;  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.MultiDisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.contentpreview.BasePreviewViewModel;  import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;  import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; +import com.android.intentresolver.contentpreview.PayloadToggleInteractor;  import com.android.intentresolver.contentpreview.PreviewViewModel; +import com.android.intentresolver.emptystate.CompositeEmptyStateProvider; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;  import com.android.intentresolver.emptystate.EmptyState;  import com.android.intentresolver.emptystate.EmptyStateProvider;  import com.android.intentresolver.grid.ChooserGridAdapter; @@ -107,28 +140,50 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator;  import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;  import com.android.intentresolver.shortcuts.AppPredictorFactory;  import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.intentresolver.v2.data.repository.DevicePolicyResources; +import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider;  import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;  import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.platform.AppPredictionAvailable;  import com.android.intentresolver.v2.platform.ImageEditor;  import com.android.intentresolver.v2.platform.NearbyShare; +import com.android.intentresolver.v2.profiles.ChooserMultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.TabConfig; +import com.android.intentresolver.v2.ui.ActionTitle; +import com.android.intentresolver.v2.ui.ShareResultSender; +import com.android.intentresolver.v2.ui.ShareResultSenderFactory; +import com.android.intentresolver.v2.ui.model.ActivityModel; +import com.android.intentresolver.v2.ui.model.ChooserRequest; +import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel;  import com.android.intentresolver.widget.ImagePreviewView; +import com.android.intentresolver.widget.ResolverDrawerLayout;  import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.content.PackageMonitor; +import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.LatencyTracker; + +import com.google.common.collect.ImmutableList;  import dagger.hilt.android.AndroidEntryPoint; +import kotlin.Pair;  import kotlin.Unit; -import java.text.Collator;  import java.util.ArrayList; +import java.util.Arrays;  import java.util.Collections; -import java.util.Comparator;  import java.util.HashMap;  import java.util.List;  import java.util.Map;  import java.util.Objects;  import java.util.Optional; +import java.util.Set;  import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors;  import java.util.concurrent.atomic.AtomicLong; @@ -142,9 +197,9 @@ import javax.inject.Inject;   *   */  @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -@AndroidEntryPoint(ResolverActivity.class) +@AndroidEntryPoint(FragmentActivity.class)  public class ChooserActivity extends Hilt_ChooserActivity implements -        ResolverListAdapter.ResolverListCommunicator { +        ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem {      private static final String TAG = "ChooserActivity";      /** @@ -167,6 +222,41 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";      private static final String SHORTCUT_TARGET = "shortcut_target"; +    ////////////////////////////////////////////////////////////////////////////////////////////// +    // Inherited properties. +    ////////////////////////////////////////////////////////////////////////////////////////////// +    private static final String TAB_TAG_PERSONAL = "personal"; +    private static final String TAB_TAG_WORK = "work"; + +    private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key"; +    protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser"; + +    private int mLayoutId; +    private UserHandle mHeaderCreatorUser; +    protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; +    protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; +    private boolean mRegistered; +    private PackageMonitor mPersonalPackageMonitor; +    private PackageMonitor mWorkPackageMonitor; +    protected View mProfileView; + +    protected ActivityLogic mLogic; +    protected ResolverDrawerLayout mResolverDrawerLayout; +    protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; +    protected final LatencyTracker mLatencyTracker = getLatencyTracker(); + +    /** See {@link #setRetainInOnStop}. */ +    private boolean mRetainInOnStop; +    protected Insets mSystemWindowInsets = null; +    private ResolverActivity.PickTargetOptionRequest mPickOptionRequest; + +    @Nullable +    private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + +    ////////////////////////////////////////////////////////////////////////////////////////////// +    ////////////////////////////////////////////////////////////////////////////////////////////// + +      // TODO: these data structures are for one-time use in shuttling data from where they're      // populated in `ShortcutToChooserTargetConverter` to where they're consumed in      // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. @@ -184,11 +274,21 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;      private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; +    @Inject public ChooserHelper mChooserHelper;      @Inject public FeatureFlags mFeatureFlags; +    @Inject public android.service.chooser.FeatureFlags mChooserServiceFeatureFlags;      @Inject public EventLog mEventLog; +    @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable;      @Inject @ImageEditor public Optional<ComponentName> mImageEditor;      @Inject @NearbyShare public Optional<ComponentName> mNearbyShare;      @Inject public TargetDataLoader mTargetDataLoader; +    @Inject public DevicePolicyResources mDevicePolicyResources; +    @Inject public PackageManager mPackageManager; +    @Inject public ClipboardManager mClipboardManager; +    @Inject public IntentForwarding mIntentForwarding; +    @Inject public ShareResultSenderFactory mShareResultSenderFactory; +    @Nullable +    private ShareResultSender mShareResultSender;      private ChooserRefinementManager mRefinementManager; @@ -216,14 +316,12 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      private int mScrollStatus = SCROLL_STATUS_IDLE; -    @VisibleForTesting -    protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;      private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =              new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); -    private View mContentView = null; +    private final View mContentView = null; -    private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>(); +    private final Map<Integer, ProfileRecord> mProfileRecords = new HashMap<>();      private boolean mExcludeSharedText = false;      /** @@ -236,35 +334,260 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); +    protected ActivityModel createActivityModel() { +        return ActivityModel.createFrom(this); +    } + +    private ChooserViewModel mViewModel; +    private ActivityModel mActivityModel; + +    @VisibleForTesting +    protected ChooserActivityLogic createActivityLogic() { +        return new ChooserActivityLogic( +                TAG, +                /* activity = */ this, +                this::onWorkProfileStatusUpdated); +    } + +    @NonNull +    @Override +    public CreationExtras getDefaultViewModelCreationExtras() { +        return addDefaultArgs( +                super.getDefaultViewModelCreationExtras(), +                new Pair<>(ACTIVITY_MODEL_KEY, createActivityModel())); +    } +      @Override      protected void onCreate(Bundle savedInstanceState) { -        Tracer.INSTANCE.markLaunched();          super.onCreate(savedInstanceState); -        setLogic(new ChooserActivityLogic( -                TAG, -                () -> this, -                this::onWorkProfileStatusUpdated, -                () -> mTargetDataLoader, -                this::onPreinitialization)); -        addInitializer(this::init); -    } +        Log.i(TAG, "onCreate"); +        mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); +        mActivityModel = mViewModel.getActivityModel(); -    private void init() { -        if (getChooserRequest() == null) { +        int callerUid = mActivityModel.getLaunchedFromUid(); +        if (callerUid < 0 || UserHandle.isIsolated(callerUid)) { +            Log.e(TAG, "Can't start a resolver from uid " + callerUid); +            finish(); +        } + +        setTheme(R.style.Theme_DeviceDefault_Chooser); +        Tracer.INSTANCE.markLaunched(); +        if (!mViewModel.init()) {              finish();              return;          } + +        // The post-create callback is invoked when this function returns, via Lifecycle. +        mChooserHelper.setPostCreateCallback(this::init); + +        IntentSender chosenComponentSender = +                mViewModel.getChooserRequest().getChosenComponentSender(); +        if (chosenComponentSender != null) { +            mShareResultSender = mShareResultSenderFactory +                    .create(mActivityModel.getLaunchedFromUid(), chosenComponentSender); +        } +        mLogic = createActivityLogic(); +    } + +    @Override +    protected final void onStart() { +        super.onStart(); + +        this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); +        if (hasWorkProfile()) { +            mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this); +        } +    } + +    @Override +    protected final void onResume() { +        super.onResume(); +        Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); +        mFinishWhenStopped = false; +        mRefinementManager.onActivityResume(); +    } + +    @Override +    protected final void onStop() { +        super.onStop(); + +        final Window window = this.getWindow(); +        final WindowManager.LayoutParams attrs = window.getAttributes(); +        attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +        window.setAttributes(attrs); + +        if (mRegistered) { +            mPersonalPackageMonitor.unregister(); +            if (mWorkPackageMonitor != null) { +                mWorkPackageMonitor.unregister(); +            } +            mRegistered = false; +        } +        final Intent intent = getIntent(); +        if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() +                && !mRetainInOnStop) { +            // This resolver is in the unusual situation where it has been +            // launched at the top of a new task.  We don't let it be added +            // to the recent tasks shown to the user, and we need to make sure +            // that each time we are launched we get the correct launching +            // uid (not re-using the same resolver from an old launching uid), +            // so we will now finish ourself since being no longer visible, +            // the user probably can't get back to us. +            if (!isChangingConfigurations()) { +                finish(); +            } +        } +        mLogic.getWorkProfileAvailabilityManager().unregisterWorkProfileStateReceiver(this); + +        if (mRefinementManager != null) { +            mRefinementManager.onActivityStop(isChangingConfigurations()); +        } + +        if (mFinishWhenStopped) { +            mFinishWhenStopped = false; +            finish(); +        } +    } + +    @Override +    protected final void onSaveInstanceState(Bundle outState) { +        super.onSaveInstanceState(outState); +        ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); +        if (viewPager != null) { +            outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem()); +        } +    } + +    @Override +    protected final void onRestart() { +        super.onRestart(); +        if (!mRegistered) { +            mPersonalPackageMonitor.register( +                    this, +                    getMainLooper(), +                    requireAnnotatedUserHandles().personalProfileUserHandle, +                    false); +            if (hasWorkProfile()) { +                if (mWorkPackageMonitor == null) { +                    mWorkPackageMonitor = createPackageMonitor( +                            mChooserMultiProfilePagerAdapter.getWorkListAdapter()); +                } +                mWorkPackageMonitor.register( +                        this, +                        getMainLooper(), +                        requireAnnotatedUserHandles().workProfileUserHandle, +                        false); +            } +            mRegistered = true; +        } +        WorkProfileAvailabilityManager workProfileAvailabilityManager = +                mLogic.getWorkProfileAvailabilityManager(); +        if (hasWorkProfile() && workProfileAvailabilityManager.isWaitingToEnableWorkProfile()) { +            if (workProfileAvailabilityManager.isQuietModeEnabled()) { +                workProfileAvailabilityManager.markWorkProfileEnabledBroadcastReceived(); +            } +        } +        mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); +    } + +    @Override +    protected void onDestroy() { +        super.onDestroy(); +          if (isFinishing()) { -            // Performing a clean exit: -            //    Skip initializing any additional resources. -            return; +            mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);          } -        setTheme(mLogic.getThemeResId()); -        getEventLog().logSharesheetTriggered(); +        mBackgroundThreadPoolExecutor.shutdownNow(); -        mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); +        destroyProfileRecords(); +    } + +    private void init() { +        mIntentReceivedTime.set(System.currentTimeMillis()); +        mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); + +        mPinnedSharedPrefs = getPinnedSharedPrefs(this); +        mMaxTargetsPerRow = +                getResources().getInteger(R.integer.config_chooser_max_targets_per_row); +        mShouldDisplayLandscape = +                shouldDisplayLandscape(getResources().getConfiguration().orientation); + +        ChooserRequest chooserRequest = mViewModel.getChooserRequest(); +        setRetainInOnStop(chooserRequest.shouldRetainInOnStop()); +        createProfileRecords( +                new AppPredictorFactory( +                        this, +                        Objects.toString(chooserRequest.getSharedText(), null), +                        chooserRequest.getShareTargetFilter(), +                        mAppPredictionAvailable +                ), +                chooserRequest.getShareTargetFilter() +        ); + +        Intent intent = mViewModel.getChooserRequest().getTargetIntent(); +        List<Intent> initialIntents = mViewModel.getChooserRequest().getInitialIntents(); + +        mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( +                requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), +                /* resolutionList = */ null, +                false +        ); +        if (!configureContentView(mTargetDataLoader)) { +            mPersonalPackageMonitor = createPackageMonitor( +                    mChooserMultiProfilePagerAdapter.getPersonalListAdapter()); +            mPersonalPackageMonitor.register( +                    this, +                    getMainLooper(), +                    requireAnnotatedUserHandles().personalProfileUserHandle, +                    false +            ); +            if (hasWorkProfile()) { +                mWorkPackageMonitor = createPackageMonitor( +                        mChooserMultiProfilePagerAdapter.getWorkListAdapter()); +                mWorkPackageMonitor.register( +                        this, +                        getMainLooper(), +                        requireAnnotatedUserHandles().workProfileUserHandle, +                        false +                ); +            } +            mRegistered = true; +            final ResolverDrawerLayout rdl = findViewById( +                    com.android.internal.R.id.contentPanel); +            if (rdl != null) { +                rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { +                    @Override +                    public void onDismissed() { +                        finish(); +                    } +                }); + +                boolean hasTouchScreen = mPackageManager +                        .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + +                if (isVoiceInteraction() || !hasTouchScreen) { +                    rdl.setCollapsed(false); +                } +                rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION +                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); +                rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets); + +                mResolverDrawerLayout = rdl; +            } +            final Set<String> categories = intent.getCategories(); +            MetricsLogger.action(this, +                    mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() +                            ? MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED +                            : MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED, +                    intent.getAction() + ":" + intent.getType() + ":" +                            + (categories != null ? Arrays.toString(categories.toArray()) +                            : "")); +        } + +        getEventLog().logSharesheetTriggered(); +        mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);          mRefinementManager.getRefinementCompletion().observe(this, completion -> {              if (completion.consume()) {                  TargetInfo targetInfo = completion.getTargetInfo(); @@ -276,26 +599,56 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                      // can't recover a Chooser session if that's the reason the refined target fails                      // to launch now. Fire-and-forget the refined launch; ignore the return value                      // and just make sure the Sharesheet session gets cleaned up regardless. -                    ChooserActivity.super.onTargetSelected(targetInfo, false); +                    final ResolveInfo ri = targetInfo.getResolveInfo(); +                    final Intent intent1 = targetInfo.getResolvedIntent(); + +                    safelyStartActivity(targetInfo); + +                    // Rely on the ActivityManager to pop up a dialog regarding app suspension +                    // and return false +                    targetInfo.isSuspended();                  }                  finish();              }          }); -          BasePreviewViewModel previewViewModel =                  new ViewModelProvider(this, createPreviewViewModelFactory())                          .get(BasePreviewViewModel.class); -        ChooserRequestParameters chooserRequest = requireChooserRequest(); +        previewViewModel.init( +                chooserRequest.getTargetIntent(), +                mActivityModel.getIntent(), +                chooserRequest.getAdditionalContentUri(), +                chooserRequest.getFocusedItemPosition(), +                mChooserServiceFeatureFlags.chooserPayloadToggling()); +        ChooserActionFactory chooserActionFactory = createChooserActionFactory(); +        ChooserContentPreviewUi.ActionFactory actionFactory = chooserActionFactory; +        if (previewViewModel.getPreviewDataProvider().getPreviewType() +                == CONTENT_PREVIEW_PAYLOAD_SELECTION +                && mChooserServiceFeatureFlags.chooserPayloadToggling()) { +            PayloadToggleInteractor payloadToggleInteractor = +                    previewViewModel.getPayloadToggleInteractor(); +            if (payloadToggleInteractor != null) { +                ChooserMutableActionFactory mutableActionFactory = +                        new ChooserMutableActionFactory(chooserActionFactory); +                actionFactory = mutableActionFactory; +                JavaFlowHelper.collect( +                        getCoroutineScope(getLifecycle()), +                        payloadToggleInteractor.getCustomActions(), +                        mutableActionFactory::updateCustomActions); +            } +        }          mChooserContentPreviewUi = new ChooserContentPreviewUi(                  getCoroutineScope(getLifecycle()), -                previewViewModel.createOrReuseProvider(chooserRequest.getTargetIntent()), +                previewViewModel.getPreviewDataProvider(),                  chooserRequest.getTargetIntent(), -                previewViewModel.createOrReuseImageLoader(), -                createChooserActionFactory(), +                previewViewModel.getImageLoader(), +                actionFactory,                  mEnterTransitionAnimationDelegate, -                new HeadlineGeneratorImpl(this)); - +                new HeadlineGeneratorImpl(this), +                chooserRequest.getContentTypeHint(), +                chooserRequest.getMetadataText(), +                mChooserServiceFeatureFlags.chooserPayloadToggling());          updateStickyContentPreview();          if (shouldShowStickyContentPreview()                  || mChooserMultiProfilePagerAdapter @@ -303,12 +656,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              getEventLog().logActionShareWithPreview(                      mChooserContentPreviewUi.getPreferredContentPreview());          } -          mChooserShownTime = System.currentTimeMillis();          final long systemCost = mChooserShownTime - mIntentReceivedTime.get();          getEventLog().logChooserActivityShown(                  isWorkProfile(), chooserRequest.getTargetType(), systemCost); -          if (mResolverDrawerLayout != null) {              mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); @@ -318,64 +669,605 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                          getEventLog().logSharesheetExpansionChanged(isCollapsed);                      });          } -          if (DEBUG) {              Log.d(TAG, "System Time Cost is " + systemCost);          } -          getEventLog().logShareStarted( -                mLogic.getReferrerPackageName(), +                chooserRequest.getReferrerPackage(),                  chooserRequest.getTargetType(),                  chooserRequest.getCallerChooserTargets().size(), -                (chooserRequest.getInitialIntents() == null) -                        ? 0 : chooserRequest.getInitialIntents().length, +                chooserRequest.getInitialIntents().size(),                  isWorkProfile(),                  mChooserContentPreviewUi.getPreferredContentPreview(),                  chooserRequest.getTargetAction(),                  chooserRequest.getChooserActions().size(),                  chooserRequest.getModifyShareAction() != null          ); -          mEnterTransitionAnimationDelegate.postponeTransition();      } -    protected final Unit onPreinitialization() { -        mIntentReceivedTime.set(System.currentTimeMillis()); -        mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); +    private void restore(@Nullable Bundle savedInstanceState) { +        if (savedInstanceState != null) { +            // onRestoreInstanceState +            //resetButtonBar(); +            ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); +            if (viewPager != null) { +                viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); +            } +        } -        mPinnedSharedPrefs = getPinnedSharedPrefs(this); -        mMaxTargetsPerRow = -                getResources().getInteger(R.integer.config_chooser_max_targets_per_row); -        mShouldDisplayLandscape = -                shouldDisplayLandscape(getResources().getConfiguration().orientation); +        mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); +    } +    ////////////////////////////////////////////////////////////////////////////////////////////// +    // Inherited methods +    ////////////////////////////////////////////////////////////////////////////////////////////// -        ChooserRequestParameters chooserRequest = getChooserRequest(); -        if (chooserRequest == null) { -            return Unit.INSTANCE; +    private boolean isAutolaunching() { +        return !mRegistered && isFinishing(); +    } + +    private boolean maybeAutolaunchIfSingleTarget() { +        int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); +        if (count != 1) { +            return false;          } -        setRetainInOnStop(chooserRequest.shouldRetainInOnStop()); -        createProfileRecords( -                new AppPredictorFactory( -                        this, -                        chooserRequest.getSharedText(), -                        chooserRequest.getTargetIntentFilter() -                ), -                chooserRequest.getTargetIntentFilter() +        if (mChooserMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) { +            return false; +        } + +        // Only one target, so we're a candidate to auto-launch! +        final TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() +                .targetInfoForPosition(0, false); +        if (shouldAutoLaunchSingleChoice(target)) { +            safelyStartActivity(target); +            finish(); +            return true; +        } +        return false; +    } + + +    private boolean isTwoPagePersonalAndWorkConfiguration() { +        return (mChooserMultiProfilePagerAdapter.getCount() == 2) +                && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL) +                && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK); +    } + +    /** +     * When we have a personal and a work profile, we auto launch in the following scenario: +     * - There is 1 resolved target on each profile +     * - That target is the same app on both profiles +     * - The target app has permission to communicate cross profiles +     * - The target app has declared it supports cross-profile communication via manifest metadata +     */ +    private boolean maybeAutolaunchIfCrossProfileSupported() { +        if (!isTwoPagePersonalAndWorkConfiguration()) { +            return false; +        } + +        ResolverListAdapter activeListAdapter = +                (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                        ? mChooserMultiProfilePagerAdapter.getPersonalListAdapter() +                        : mChooserMultiProfilePagerAdapter.getWorkListAdapter(); + +        ResolverListAdapter inactiveListAdapter = +                (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                        ? mChooserMultiProfilePagerAdapter.getWorkListAdapter() +                        : mChooserMultiProfilePagerAdapter.getPersonalListAdapter(); + +        if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) { +            return false; +        } + +        if ((activeListAdapter.getUnfilteredCount() != 1) +                || (inactiveListAdapter.getUnfilteredCount() != 1)) { +            return false; +        } + +        TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false); +        TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false); +        if (!Objects.equals( +                activeProfileTarget.getResolvedComponentName(), +                inactiveProfileTarget.getResolvedComponentName())) { +            return false; +        } + +        if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) { +            return false; +        } + +        String packageName = activeProfileTarget.getResolvedComponentName().getPackageName(); +        if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) { +            return false; +        } + +        DevicePolicyEventLogger +                .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET) +                .setBoolean(activeListAdapter.getUserHandle() +                        .equals(requireAnnotatedUserHandles().personalProfileUserHandle)) +                .setStrings(getMetricsCategory()) +                .write(); +        safelyStartActivity(activeProfileTarget); +        finish(); +        return true; +    } + +    /** +     * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} +     */ +    private boolean maybeAutolaunchActivity() { +        int numberOfProfiles = mChooserMultiProfilePagerAdapter.getItemCount(); +        // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the +        //  correct intent-picker UIs (e.g., mini-resolver) if it was launched without +        //  ACTION_SEND. +        if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) { +            return true; +        } else if (maybeAutolaunchIfCrossProfileSupported()) { +            return true; +        } +        return false; +    } + +    @Override // ResolverListCommunicator +    public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, +            boolean rebuildCompleted) { +        if (isAutolaunching()) { +            return; +        } +        if (mChooserMultiProfilePagerAdapter +                .shouldShowEmptyStateScreen((ChooserListAdapter) listAdapter)) { +            mChooserMultiProfilePagerAdapter +                    .showEmptyResolverListEmptyState((ChooserListAdapter) listAdapter); +        } else { +            mChooserMultiProfilePagerAdapter.showListView((ChooserListAdapter) listAdapter); +        } +        // showEmptyResolverListEmptyState can mark the tab as loaded, +        // which is a precondition for auto launching +        if (rebuildCompleted && maybeAutolaunchActivity()) { +            return; +        } +        if (doPostProcessing) { +            maybeCreateHeader(listAdapter); +            onListRebuilt(listAdapter, rebuildCompleted); +        } +    } + +    private CharSequence getOrLoadDisplayLabel(TargetInfo info) { +        if (info.isDisplayResolveInfo()) { +            mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info); +        } +        CharSequence displayLabel = info.getDisplayLabel(); +        return displayLabel == null ? "" : displayLabel; +    } + +    protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { +        final ActionTitle title = ActionTitle.forAction(intent.getAction()); + +        // While there may already be a filtered item, we can only use it in the title if the list +        // is already sorted and all information relevant to it is already in the list. +        final boolean named = +                mChooserMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0; +        if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { +            return getString(defaultTitleRes); +        } else { +            return named +                    ? getString( +                    title.namedTitleRes, +                    getOrLoadDisplayLabel( +                            mChooserMultiProfilePagerAdapter +                                    .getActiveListAdapter().getFilteredItem())) +                    : getString(title.titleRes); +        } +    } + +    /** +     * Configure the area above the app selection list (title, content preview, etc). +     */ +    private void maybeCreateHeader(ResolverListAdapter listAdapter) { +        if (mHeaderCreatorUser != null +                && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) { +            return; +        } +        if (!hasWorkProfile() +                && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) { +            final TextView titleView = findViewById(com.android.internal.R.id.title); +            if (titleView != null) { +                titleView.setVisibility(View.GONE); +            } +        } + +        CharSequence title = mViewModel.getChooserRequest().getTitle() != null +                ? mViewModel.getChooserRequest().getTitle() +                : getTitleForAction(mViewModel.getChooserRequest().getTargetIntent(), +                        mViewModel.getChooserRequest().getDefaultTitleResource()); + +        if (!TextUtils.isEmpty(title)) { +            final TextView titleView = findViewById(com.android.internal.R.id.title); +            if (titleView != null) { +                titleView.setText(title); +            } +            setTitle(title); +        } + +        final ImageView iconView = findViewById(com.android.internal.R.id.icon); +        if (iconView != null) { +            listAdapter.loadFilteredItemIconTaskAsync(iconView); +        } +        mHeaderCreatorUser = listAdapter.getUserHandle(); +    } + +    /** Start the activity specified by the {@link TargetInfo}.*/ +    public final void safelyStartActivity(TargetInfo cti) { +        // In case cloned apps are present, we would want to start those apps in cloned user +        // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle +        // identifies the correct user space in such cases. +        UserHandle activityUserHandle = cti.getResolveInfo().userHandle; +        safelyStartActivityAsUser(cti, activityUserHandle, null); +    } + +    protected final void safelyStartActivityAsUser( +            TargetInfo cti, UserHandle user, @Nullable Bundle options) { +        // We're dispatching intents that might be coming from legacy apps, so +        // don't kill ourselves. +        StrictMode.disableDeathOnFileUriExposure(); +        try { +            safelyStartActivityInternal(cti, user, options); +        } finally { +            StrictMode.enableDeathOnFileUriExposure(); +        } +    } + +    @VisibleForTesting +    protected void safelyStartActivityInternal( +            TargetInfo cti, UserHandle user, @Nullable Bundle options) { +        // If the target is suspended, the activity will not be successfully launched. +        // Do not unregister from package manager updates in this case +        if (!cti.isSuspended() && mRegistered) { +            if (mPersonalPackageMonitor != null) { +                mPersonalPackageMonitor.unregister(); +            } +            if (mWorkPackageMonitor != null) { +                mWorkPackageMonitor.unregister(); +            } +            mRegistered = false; +        } +        // If needed, show that intent is forwarded +        // from managed profile to owner or other way around. +        String profileSwitchMessage = mIntentForwarding.forwardMessageFor( +                mViewModel.getChooserRequest().getTargetIntent()); +        if (profileSwitchMessage != null) { +            Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); +        } +        try { +            if (cti.startAsCaller(this, options, user.getIdentifier())) { +                maybeSendShareResult(cti); +                maybeLogCrossProfileTargetLaunch(cti, user); +            } +        } catch (RuntimeException e) { +            Slog.wtf(TAG, +                    "Unable to launch as uid " + mActivityModel.getLaunchedFromUid() +                            + " package " + mActivityModel.getLaunchedFromPackage() + +                            ", while running in " + ActivityThread.currentProcessName(), e); +        } +    } + +    private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) { +        if (!hasWorkProfile() || currentUserHandle.equals(getUser())) { +            return; +        } +        DevicePolicyEventLogger +                .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED) +                .setBoolean( +                        currentUserHandle.equals( +                                requireAnnotatedUserHandles().personalProfileUserHandle)) +                .setStrings(getMetricsCategory(), +                        cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target") +                .write(); +    } + +    private boolean hasWorkProfile() { +        return requireAnnotatedUserHandles().workProfileUserHandle != null; +    } +    private LatencyTracker getLatencyTracker() { +        return LatencyTracker.getInstance(this); +    } + +    /** +     * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets +     * called and we are launched in a new task. +     */ +    protected final void setRetainInOnStop(boolean retainInOnStop) { +        mRetainInOnStop = retainInOnStop; +    } + +    // @NonFinalForTesting +    @VisibleForTesting +    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { +        return new CrossProfileIntentsChecker(getContentResolver()); +    } + +    protected final EmptyStateProvider createEmptyStateProvider( +            @Nullable UserHandle workProfileUserHandle) { +        final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider(); + +        final EmptyStateProvider workProfileOffEmptyStateProvider = +                new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle, +                        mLogic.getWorkProfileAvailabilityManager(), +                        /* onSwitchOnWorkSelectedListener= */ +                        () -> { +                            if (mOnSwitchOnWorkSelectedListener != null) { +                                mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); +                            } +                        }, +                        getMetricsCategory()); + +        final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( +                this, +                workProfileUserHandle, +                requireAnnotatedUserHandles().personalProfileUserHandle, +                getMetricsCategory(), +                requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch +        ); + +        // Return composite provider, the order matters (the higher, the more priority) +        return new CompositeEmptyStateProvider( +                blockerEmptyStateProvider, +                workProfileOffEmptyStateProvider, +                noAppsEmptyStateProvider          ); -        return Unit.INSTANCE;      } -    @Nullable -    private ChooserRequestParameters getChooserRequest() { -        return ((ChooserActivityLogic) mLogic).getChooserRequestParameters(); +    private boolean supportsManagedProfiles(ResolveInfo resolveInfo) { +        try { +            ApplicationInfo appInfo = mPackageManager.getApplicationInfo( +                    resolveInfo.activityInfo.packageName, 0 /* default flags */); +            return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP; +        } catch (PackageManager.NameNotFoundException e) { +            return false; +        } +    } + +    private boolean hasManagedProfile() { +        UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); +        if (userManager == null) { +            return false; +        } + +        try { +            List<UserInfo> profiles = userManager.getProfiles(getUserId()); +            for (UserInfo userInfo : profiles) { +                if (userInfo != null && userInfo.isManagedProfile()) { +                    return true; +                } +            } +        } catch (SecurityException e) { +            return false; +        } +        return false; +    } + +    /** +     * Returns the {@link UserHandle} to use when querying resolutions for intents in a +     * {@link ResolverListController} configured for the provided {@code userHandle}. +     */ +    protected final UserHandle getQueryIntentsUser(UserHandle userHandle) { +        return requireAnnotatedUserHandles().getQueryIntentsUser(userHandle); +    } + +    protected final boolean isLaunchedAsCloneProfile() { +        UserHandle launchUser = requireAnnotatedUserHandles().userHandleSharesheetLaunchedAs; +        UserHandle cloneUser = requireAnnotatedUserHandles().cloneProfileUserHandle; +        return hasCloneProfile() && launchUser.equals(cloneUser); +    } + +    private boolean hasCloneProfile() { +        return requireAnnotatedUserHandles().cloneProfileUserHandle != null; +    } + +    /** +     * Returns the {@link List} of {@link UserHandle} to pass on to the +     * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}. +     */ +    @VisibleForTesting(visibility = PROTECTED) +    public final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) { +        return getResolverRankerServiceUserHandleListInternal(userHandle); +    } + + +    @VisibleForTesting +    protected List<UserHandle> getResolverRankerServiceUserHandleListInternal( +            UserHandle userHandle) { +        List<UserHandle> userList = new ArrayList<>(); +        userList.add(userHandle); +        // Add clonedProfileUserHandle to the list only if we are: +        // a. Building the Personal Tab. +        // b. CloneProfile exists on the device. +        if (userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle) +                && hasCloneProfile()) { +            userList.add(requireAnnotatedUserHandles().cloneProfileUserHandle); +        } +        return userList; +    } + +    /** +     * Start activity as a fixed user handle. +     * @param cti TargetInfo to be launched. +     * @param user User to launch this activity as. +     */ +    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) +    public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) { +        safelyStartActivityAsUser(cti, user, null); +    } + +    protected WindowInsets super_onApplyWindowInsets(View v, WindowInsets insets) { +        mSystemWindowInsets = insets.getSystemWindowInsets(); + +        mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, +                mSystemWindowInsets.right, 0); + +        // Need extra padding so the list can fully scroll up +        // To accommodate for window insets +        applyFooterView(mSystemWindowInsets.bottom); + +        return insets.consumeSystemWindowInsets(); +    } + +    @Override // ResolverListCommunicator +    public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) { +        if (!mChooserMultiProfilePagerAdapter.onHandlePackagesChanged( +                (ChooserListAdapter) listAdapter, +                mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile())) { +            // We no longer have any items... just finish the activity. +            finish(); +        } +    } + +    final Option optionForChooserTarget(TargetInfo target, int index) { +        return new Option(getOrLoadDisplayLabel(target), index);      } -    private ChooserRequestParameters requireChooserRequest() { -        return requireNonNull(getChooserRequest()); +    @Override // ResolverListCommunicator +    public final void sendVoiceChoicesIfNeeded() { +        if (!isVoiceInteraction()) { +            // Clearly not needed. +            return; +        } + +        int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getCount(); +        final Option[] options = new Option[count]; +        for (int i = 0; i < options.length; i++) { +            TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getItem(i); +            if (target == null) { +                // If this occurs, a new set of targets is being loaded. Let that complete, +                // and have the next call to send voice choices proceed instead. +                return; +            } +            options[i] = optionForChooserTarget(target, i); +        } + +        mPickOptionRequest = new ResolverActivity.PickTargetOptionRequest( +                new VoiceInteractor.Prompt(getTitle()), options, null); +        getVoiceInteractor().submitRequest(mPickOptionRequest); +    } + +    /** +     * Sets up the content view. +     * @return <code>true</code> if the activity is finishing and creation should halt. +     */ +    private boolean configureContentView(TargetDataLoader targetDataLoader) { +        if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null) { +            throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() " +                    + "cannot be null."); +        } +        Trace.beginSection("configureContentView"); +        // We partially rebuild the inactive adapter to determine if we should auto launch +        // isTabLoaded will be true here if the empty state screen is shown instead of the list. +        boolean rebuildCompleted = mChooserMultiProfilePagerAdapter.rebuildTabs(hasWorkProfile()); + +        mLayoutId = mFeatureFlags.scrollablePreview() +                ? R.layout.chooser_grid_scrollable_preview +                : R.layout.chooser_grid; + +        setContentView(mLayoutId); +        mChooserMultiProfilePagerAdapter.setupViewPager( +                requireViewById(com.android.internal.R.id.profile_pager)); +        boolean result = postRebuildList(rebuildCompleted); +        Trace.endSection(); +        return result; +    } + +    /** +     * Finishing procedures to be performed after the list has been rebuilt. +     * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList. +     * @param rebuildCompleted +     * @return <code>true</code> if the activity is finishing and creation should halt. +     */ +    protected boolean postRebuildList(boolean rebuildCompleted) { +        return postRebuildListInternal(rebuildCompleted); +    } + +    /** +     * Add a label to signify that the user can pick a different app. +     * @param adapter The adapter used to provide data to item views. +     */ +    public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { +        final boolean useHeader = adapter.hasFilteredItem(); +        if (useHeader) { +            FrameLayout stub = findViewById(com.android.internal.R.id.stub); +            stub.setVisibility(View.VISIBLE); +            TextView textView = (TextView) LayoutInflater.from(this).inflate( +                    R.layout.resolver_different_item_header, null, false); +            if (hasWorkProfile()) { +                textView.setGravity(Gravity.CENTER); +            } +            stub.addView(textView); +        } +    } +    private void setupViewVisibilities() { +        ChooserListAdapter activeListAdapter = +                mChooserMultiProfilePagerAdapter.getActiveListAdapter(); +        if (!mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) { +            addUseDifferentAppLabelIfNecessary(activeListAdapter); +        } +    } +    /** +     * Finishing procedures to be performed after the list has been rebuilt. +     * @param rebuildCompleted +     * @return <code>true</code> if the activity is finishing and creation should halt. +     */ +    final boolean postRebuildListInternal(boolean rebuildCompleted) { +        int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); + +        // We only rebuild asynchronously when we have multiple elements to sort. In the case where +        // we're already done, we can check if we should auto-launch immediately. +        if (rebuildCompleted && maybeAutolaunchActivity()) { +            return true; +        } + +        setupViewVisibilities(); + +        if (hasWorkProfile()) { +            setupProfileTabs(); +        } + +        return false; +    } + +    private void setupProfileTabs() { +        TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost); +        ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); + +        mChooserMultiProfilePagerAdapter.setupProfileTabs( +                getLayoutInflater(), +                tabHost, +                viewPager, +                R.layout.resolver_profile_tab_button, +                com.android.internal.R.id.profile_pager, +                () -> onProfileTabSelected(viewPager.getCurrentItem()), +                new OnProfileSelectedListener() { +                    @Override +                    public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {} + +                    @Override +                    public void onProfilePageStateChanged(int state) { +                        onHorizontalSwipeStateChanged(state); +                    } +                }); +        mOnSwitchOnWorkSelectedListener = () -> { +            final View workTab = +                    tabHost.getTabWidget().getChildAt( +                            mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK)); +            workTab.setFocusable(true); +            workTab.setFocusableInTouchMode(true); +            workTab.requestFocus(); +        };      } +    ////////////////////////////////////////////////////////////////////////////////////////////// +    ////////////////////////////////////////////////////////////////////////////////////////////// +      private AnnotatedUserHandles requireAnnotatedUserHandles() {          return requireNonNull(mLogic.getAnnotatedUserHandles());      } @@ -412,7 +1304,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      @Nullable      private ProfileRecord getProfileRecord(UserHandle userHandle) { -        return mProfileRecords.get(userHandle.getIdentifier(), null); +        return mProfileRecords.get(userHandle.getIdentifier());      }      @VisibleForTesting @@ -435,25 +1327,22 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);      } -    @Override      protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(              Intent[] initialIntents,              List<ResolveInfo> rList, -            boolean filterLastUsed, -            TargetDataLoader targetDataLoader) { -        if (shouldShowTabs()) { +            boolean filterLastUsed) { +        if (hasWorkProfile()) {              mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( -                    initialIntents, rList, filterLastUsed, targetDataLoader); +                    initialIntents, rList, filterLastUsed);          } else {              mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( -                    initialIntents, rList, filterLastUsed, targetDataLoader); +                    initialIntents, rList, filterLastUsed);          }          return mChooserMultiProfilePagerAdapter;      } -    @Override      protected EmptyStateProvider createBlockerEmptyStateProvider() { -        final boolean isSendAction = requireChooserRequest().isSendActionTarget(); +        final boolean isSendAction = mViewModel.getChooserRequest().isSendActionTarget();          final EmptyState noWorkToPersonalEmptyState =                  new DevicePolicyBlockerEmptyState( @@ -492,21 +1381,27 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(              Intent[] initialIntents,              List<ResolveInfo> rList, -            boolean filterLastUsed, -            TargetDataLoader targetDataLoader) { +            boolean filterLastUsed) {          ChooserGridAdapter adapter = createChooserGridAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mViewModel.getChooserRequest().getPayloadIntents(),                  initialIntents,                  rList,                  filterLastUsed, -                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, -                targetDataLoader); +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle +        );          return new ChooserMultiProfilePagerAdapter(                  /* context */ this, -                adapter, +                ImmutableList.of( +                        new TabConfig<>( +                                PROFILE_PERSONAL, +                                mDevicePolicyResources.getPersonalTabLabel(), +                                mDevicePolicyResources.getPersonalTabAccessibilityLabel(), +                                TAB_TAG_PERSONAL, +                                adapter)),                  createEmptyStateProvider(/* workProfileUserHandle= */ null),                  /* workProfileQuietModeChecker= */ () -> false, +                /* defaultProfile= */ PROFILE_PERSONAL,                  /* workProfileUserHandle= */ null,                  requireAnnotatedUserHandles().cloneProfileUserHandle,                  mMaxTargetsPerRow, @@ -516,29 +1411,39 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(              Intent[] initialIntents,              List<ResolveInfo> rList, -            boolean filterLastUsed, -            TargetDataLoader targetDataLoader) { +            boolean filterLastUsed) {          int selectedProfile = findSelectedProfile();          ChooserGridAdapter personalAdapter = createChooserGridAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mViewModel.getChooserRequest().getPayloadIntents(),                  selectedProfile == PROFILE_PERSONAL ? initialIntents : null,                  rList,                  filterLastUsed, -                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, -                targetDataLoader); +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle +        );          ChooserGridAdapter workAdapter = createChooserGridAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mViewModel.getChooserRequest().getPayloadIntents(),                  selectedProfile == PROFILE_WORK ? initialIntents : null,                  rList,                  filterLastUsed, -                /* userHandle */ requireAnnotatedUserHandles().workProfileUserHandle, -                targetDataLoader); +                /* userHandle */ requireAnnotatedUserHandles().workProfileUserHandle +        );          return new ChooserMultiProfilePagerAdapter(                  /* context */ this, -                personalAdapter, -                workAdapter, +                ImmutableList.of( +                        new TabConfig<>( +                                PROFILE_PERSONAL, +                                mDevicePolicyResources.getPersonalTabLabel(), +                                mDevicePolicyResources.getPersonalTabAccessibilityLabel(), +                                TAB_TAG_PERSONAL, +                                personalAdapter), +                        new TabConfig<>( +                                PROFILE_WORK, +                                mDevicePolicyResources.getWorkTabLabel(), +                                mDevicePolicyResources.getWorkTabAccessibilityLabel(), +                                TAB_TAG_WORK, +                                workAdapter)),                  createEmptyStateProvider(requireAnnotatedUserHandles().workProfileUserHandle),                  () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),                  selectedProfile, @@ -549,12 +1454,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      }      private int findSelectedProfile() { -        int selectedProfile = getSelectedProfileExtra(); -        if (selectedProfile == -1) { -            selectedProfile = getProfileForUser( -                    requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch); -        } -        return selectedProfile; +        return getProfileForUser(requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);      }      /** @@ -567,7 +1467,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  .getUserInfo(UserHandle.myUserId()).isManagedProfile();      } -    @Override +    //@Override      protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {          return new PackageMonitor() {              @Override @@ -580,6 +1480,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      /**       * Update UI to reflect changes in data.       */ +    @Override      public void handlePackagesChanged() {          handlePackagesChanged(/* listAdapter */ null);      } @@ -597,23 +1498,20 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          } else {              listAdapter.handlePackagesChanged();          } -        updateProfileViewButton(); -    } - -    @Override -    protected void onResume() { -        super.onResume(); -        Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); -        mFinishWhenStopped = false; -        mRefinementManager.onActivityResume();      }      @Override      public void onConfigurationChanged(Configuration newConfig) {          super.onConfigurationChanged(newConfig); +        mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); + +        if (mSystemWindowInsets != null) { +            mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, +                    mSystemWindowInsets.right, 0); +        }          ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);          if (viewPager.isLayoutRtl()) { -            mMultiProfilePagerAdapter.setupViewPager(viewPager); +            mChooserMultiProfilePagerAdapter.setupViewPager(viewPager);          }          mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); @@ -643,7 +1541,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      }      private void updateTabPadding() { -        if (shouldShowTabs()) { +        if (hasWorkProfile()) {              View tabs = findViewById(com.android.internal.R.id.tabs);              float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);              // The entire width consists of icons or padding. Divide the item padding in half to get @@ -703,45 +1601,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return resolver.query(uri, null, null, null, null);      } -    @Override -    protected void onStop() { -        super.onStop(); -        if (mRefinementManager != null) { -            mRefinementManager.onActivityStop(isChangingConfigurations()); -        } - -        if (mFinishWhenStopped) { -            mFinishWhenStopped = false; -            finish(); -        } -    } - -    @Override -    protected void onDestroy() { -        super.onDestroy(); - -        if (isFinishing()) { -            mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); -        } - -        mBackgroundThreadPoolExecutor.shutdownNow(); - -        destroyProfileRecords(); -    } -      private void destroyProfileRecords() { -        for (int i = 0; i < mProfileRecords.size(); ++i) { -            mProfileRecords.valueAt(i).destroy(); -        } +        mProfileRecords.values().forEach(ProfileRecord::destroy);          mProfileRecords.clear();      }      @Override // ResolverListCommunicator      public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { -        ChooserRequestParameters chooserRequest = getChooserRequest(); -        if (chooserRequest == null) { -            return defIntent; -        } +        ChooserRequest chooserRequest = mViewModel.getChooserRequest();          Intent result = defIntent;          if (chooserRequest.getReplacementExtras() != null) { @@ -765,32 +1632,20 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return result;      } -    @Override -    public void onActivityStarted(TargetInfo cti) { -        ChooserRequestParameters chooserRequest = requireChooserRequest(); -        if (chooserRequest.getChosenComponentSender() != null) { +    private void maybeSendShareResult(TargetInfo cti) { +        if (mShareResultSender != null) {              final ComponentName target = cti.getResolvedComponentName();              if (target != null) { -                final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); -                try { -                    chooserRequest.getChosenComponentSender().sendIntent( -                            this, Activity.RESULT_OK, fillIn, null, null); -                } catch (IntentSender.SendIntentException e) { -                    Slog.e(TAG, "Unable to launch supplied IntentSender to report " -                            + "the chosen component: " + e); -                } +                mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo());              }          }      }      private void addCallerChooserTargets() { -        ChooserRequestParameters chooserRequest = requireChooserRequest(); +        ChooserRequest chooserRequest = mViewModel.getChooserRequest();          if (!chooserRequest.getCallerChooserTargets().isEmpty()) {              // Send the caller's chooser targets only to the default profile. -            UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK) -                    ? requireAnnotatedUserHandles().workProfileUserHandle -                    : requireAnnotatedUserHandles().personalProfileUserHandle; -            if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) { +            if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) {                  mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(                          /* origTarget */ null,                          new ArrayList<>(chooserRequest.getCallerChooserTargets()), @@ -801,28 +1656,18 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          }      } -    @Override -    public int getLayoutResource() { -        return mFeatureFlags.scrollablePreview() -                ? R.layout.chooser_grid_scrollable_preview -                : R.layout.chooser_grid; -    } -      @Override // ResolverListCommunicator      public boolean shouldGetActivityMetadata() {          return true;      } -    @Override      public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { -        // Note that this is only safe because the Intent handled by the ChooserActivity is -        // guaranteed to contain no extras unknown to the local ClassLoader. That is why this -        // method can not be replaced in the ResolverActivity whole hog. -        if (!super.shouldAutoLaunchSingleChoice(target)) { +        if (target.isSuspended()) {              return false;          } -        return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); +        return mActivityModel.getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, +                true);      }      private void showTargetDetails(TargetInfo targetInfo) { @@ -837,8 +1682,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          // TODO: implement these type-conditioned behaviors polymorphically, and consider moving          // the logic into `ChooserTargetActionsDialogFragment.show()`.          boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); -        IntentFilter intentFilter = targetInfo.isSelectableTargetInfo() -                ? requireChooserRequest().getTargetIntentFilter() : null; +        IntentFilter intentFilter; +        intentFilter = targetInfo.isSelectableTargetInfo() +                ? mViewModel.getChooserRequest().getShareTargetFilter() : null;          String shortcutTitle = targetInfo.isSelectableTargetInfo()                  ? targetInfo.getDisplayLabel().toString() : null;          String shortcutIdKey = targetInfo.getDirectShareShortcutId(); @@ -855,22 +1701,25 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  intentFilter);      } -    @Override -    protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { +    protected boolean onTargetSelected(TargetInfo target) {          if (mRefinementManager.maybeHandleSelection(                  target, -                requireChooserRequest().getRefinementIntentSender(), +                mViewModel.getChooserRequest().getRefinementIntentSender(),                  getApplication(),                  getMainThreadHandler())) {              return false;          }          updateModelAndChooserCounts(target);          maybeRemoveSharedText(target); -        return super.onTargetSelected(target, alwaysCheck); +        safelyStartActivity(target); + +        // Rely on the ActivityManager to pop up a dialog regarding app suspension +        // and return false +        return !target.isSuspended();      }      @Override -    public void startSelected(int which, boolean always, boolean filtered) { +    public void startSelected(int which, /* unused */ boolean always, boolean filtered) {          ChooserListAdapter currentListAdapter =                  mChooserMultiProfilePagerAdapter.getActiveListAdapter();          TargetInfo targetInfo = currentListAdapter @@ -893,8 +1742,23 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  return;              }          } +        if (isFinishing()) { +            return; +        } -        super.startSelected(which, always, filtered); +        TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter() +                .targetInfoForPosition(which, filtered); +        if (target != null) { +            if (onTargetSelected(target)) { +                MetricsLogger.action( +                        this, MetricsEvent.ACTION_APP_DISAMBIG_TAP); +                MetricsLogger.action(this, +                        mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() +                                ? MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED +                                : MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); +                finish(); +            } +        }          // TODO: both of the conditions around this switch logic *should* be redundant, and          // can be removed if certain invariants can be guaranteed. In particular, it seems @@ -914,7 +1778,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                              targetInfo.getResolveInfo().activityInfo.processName,                              which,                              /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), -                            requireChooserRequest().getCallerChooserTargets().size(), +                            mViewModel.getChooserRequest().getCallerChooserTargets().size(),                              targetInfo.getHashedTargetIdForMetrics(this),                              targetInfo.isPinned(),                              mIsSuccessfullySelected, @@ -951,7 +1815,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                              mIsSuccessfullySelected,                              selectionCost                      ); -                    return;              }          }      } @@ -973,13 +1836,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return -1;      } -    @Override -    protected boolean shouldAddFooterView() { -        // To accommodate for window insets -        return true; -    } - -    @Override      protected void applyFooterView(int height) {          mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height);      } @@ -1001,7 +1857,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (info != null) {              sendClickToAppPredictor(info);              final ResolveInfo ri = info.getResolveInfo(); -            Intent targetIntent = mLogic.getTargetIntent(); +            Intent targetIntent = mViewModel.getChooserRequest().getTargetIntent();              if (ri != null && ri.activityInfo != null && targetIntent != null) {                  ChooserListAdapter currentListAdapter =                          mChooserMultiProfilePagerAdapter.getActiveListAdapter(); @@ -1029,7 +1885,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (targetIntent == null) {              return;          } -        Intent originalTargetIntent = new Intent(requireChooserRequest().getTargetIntent()); +        Intent originalTargetIntent = new Intent(mViewModel.getChooserRequest().getTargetIntent());          // Our TargetInfo implementations add associated component to the intent, let's do the same          // for the sake of the comparison below.          if (targetIntent.getComponent() != null) { @@ -1103,61 +1959,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  ? null : record.appPredictor;      } -    /** -     * Sort intents alphabetically based on display label. -     */ -    static class AzInfoComparator implements Comparator<DisplayResolveInfo> { -        Comparator<DisplayResolveInfo> mComparator; -        AzInfoComparator(Context context) { -            Collator collator = Collator -                        .getInstance(context.getResources().getConfiguration().locale); -            // Adding two stage comparator, first stage compares using displayLabel, next stage -            //  compares using resolveInfo.userHandle -            mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) -                    .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier()); -        } - -        @Override -        public int compare( -                DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { -            return mComparator.compare(lhsp, rhsp); -        } -    } -      protected EventLog getEventLog() {          return mEventLog;      } -    public class ChooserListController extends ResolverListController { -        public ChooserListController( -                Context context, -                PackageManager pm, -                Intent targetIntent, -                String referrerPackageName, -                int launchedFromUid, -                AbstractResolverComparator resolverComparator, -                UserHandle queryIntentsAsUser) { -            super( -                    context, -                    pm, -                    targetIntent, -                    referrerPackageName, -                    launchedFromUid, -                    resolverComparator, -                    queryIntentsAsUser); -        } - -        @Override -        public boolean isComponentFiltered(ComponentName name) { -            return requireChooserRequest().getFilteredComponentNames().contains(name); -        } - -        @Override -        public boolean isComponentPinned(ComponentName name) { -            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); -        } -    } -      @VisibleForTesting      public ChooserGridAdapter createChooserGridAdapter(              Context context, @@ -1165,9 +1970,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              Intent[] initialIntents,              List<ResolveInfo> rList,              boolean filterLastUsed, -            UserHandle userHandle, -            TargetDataLoader targetDataLoader) { -        ChooserRequestParameters parameters = requireChooserRequest(); +            UserHandle userHandle) { +        ChooserRequest request = mViewModel.getChooserRequest();          ChooserListAdapter chooserListAdapter = createChooserListAdapter(                  context,                  payloadIntents, @@ -1176,17 +1980,17 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  filterLastUsed,                  createListController(userHandle),                  userHandle, -                mLogic.getTargetIntent(), -                parameters.getReferrerFillInIntent(), -                mMaxTargetsPerRow, -                targetDataLoader); +                request.getTargetIntent(), +                request.getReferrerFillInIntent(), +                mMaxTargetsPerRow +        );          return new ChooserGridAdapter(                  context,                  new ChooserGridAdapter.ChooserActivityDelegate() {                      @Override                      public boolean shouldShowTabs() { -                        return ChooserActivity.this.shouldShowTabs(); +                        return hasWorkProfile();                      }                      @Override @@ -1212,13 +2016,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                              showTargetDetails(longPressedTargetInfo);                          }                      } - -                    @Override -                    public void updateProfileViewButton(View newButtonFromProfileRow) { -                        mProfileView = newButtonFromProfileRow; -                        mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); -                        ChooserActivity.this.updateProfileViewButton(); -                    }                  },                  chooserListAdapter,                  shouldShowContentPreview(), @@ -1237,8 +2034,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              UserHandle userHandle,              Intent targetIntent,              Intent referrerFillInIntent, -            int maxTargetsPerRow, -            TargetDataLoader targetDataLoader) { +            int maxTargetsPerRow) {          UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()                  && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)                  ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle; @@ -1253,30 +2049,35 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  targetIntent,                  referrerFillInIntent,                  this, -                context.getPackageManager(), +                mPackageManager,                  getEventLog(),                  maxTargetsPerRow,                  initialIntentsUserSpace, -                targetDataLoader, +                mTargetDataLoader,                  () -> {                      ProfileRecord record = getProfileRecord(userHandle);                      if (record != null && record.shortcutLoader != null) {                          record.shortcutLoader.reset();                      } -                }); +                }, +                mFeatureFlags);      } -    @Override      protected Unit onWorkProfileStatusUpdated() {          UserHandle workUser = requireAnnotatedUserHandles().workProfileUserHandle;          ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);          if (record != null && record.shortcutLoader != null) {              record.shortcutLoader.reset();          } -        return super.onWorkProfileStatusUpdated(); +        if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals( +                requireAnnotatedUserHandles().workProfileUserHandle)) { +            mChooserMultiProfilePagerAdapter.rebuildActiveTab(true); +        } else { +            mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); +        } +        return Unit.INSTANCE;      } -    @Override      @VisibleForTesting      protected ChooserListController createListController(UserHandle userHandle) {          AppPredictor appPredictor = getAppPredictor(userHandle); @@ -1284,8 +2085,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (appPredictor != null) {              resolverComparator = new AppPredictionServiceResolverComparator(                      this, -                    mLogic.getTargetIntent(), -                    mLogic.getReferrerPackageName(), +                    mViewModel.getChooserRequest().getTargetIntent(), +                    mViewModel.getChooserRequest().getLaunchedFromPackage(),                      appPredictor,                      userHandle,                      getEventLog(), @@ -1295,8 +2096,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              resolverComparator =                      new ResolverRankerServiceResolverComparator(                              this, -                            mLogic.getTargetIntent(), -                            mLogic.getReferrerPackageName(), +                            mViewModel.getChooserRequest().getTargetIntent(), +                            mViewModel.getChooserRequest().getReferrerPackage(),                              null,                              getEventLog(),                              getResolverRankerServiceUserHandleList(userHandle), @@ -1305,12 +2106,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return new ChooserListController(                  this, -                mPm, -                mLogic.getTargetIntent(), -                mLogic.getReferrerPackageName(), +                mPackageManager, +                mViewModel.getChooserRequest().getTargetIntent(), +                mViewModel.getChooserRequest().getReferrerPackage(),                  requireAnnotatedUserHandles().userIdOfCallingApp,                  resolverComparator, -                getQueryIntentsUser(userHandle)); +                getQueryIntentsUser(userHandle), +                mViewModel.getChooserRequest().getFilteredComponentNames(), +                mPinnedSharedPrefs);      }      @VisibleForTesting @@ -1319,11 +2122,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      }      private ChooserActionFactory createChooserActionFactory() { -        ChooserRequestParameters request = requireChooserRequest(); +        ChooserRequest request = mViewModel.getChooserRequest();          return new ChooserActionFactory(                  this,                  request.getTargetIntent(), -                request.getReferrerPackageName(), +                request.getLaunchedFromPackage(),                  request.getChooserActions(),                  request.getModifyShareAction(),                  mImageEditor, @@ -1354,12 +2157,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                          mFinishWhenStopped = true;                      }                  }, +                mShareResultSender,                  (status) -> {                      if (status != null) {                          setResult(status);                      }                      finish(); -                }); +                }, +                mClipboardManager);      }      /* @@ -1404,8 +2209,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                  updateTabPadding();              } -            UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle(); -            int currentProfile = getProfileForUser(currentUserHandle); +            int currentProfile = mChooserMultiProfilePagerAdapter.getActiveProfile();              int initialProfile = findSelectedProfile();              if (currentProfile != initialProfile) {                  return; @@ -1432,7 +2236,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;          int rowsToShow = gridAdapter.getSystemRowCount() -                + gridAdapter.getProfileRowCount()                  + gridAdapter.getServiceTargetRowCount()                  + gridAdapter.getCallerAndRankedTargetRowCount(); @@ -1455,7 +2258,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              offset += stickyContentPreview.getHeight();          } -        if (shouldShowTabs()) { +        if (hasWorkProfile()) {              offset += findViewById(com.android.internal.R.id.tabs).getHeight();          } @@ -1512,7 +2315,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return PROFILE_PERSONAL;      } -    @Override      protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {          setupScrollListener();          maybeSetupGlobalLayoutListener(); @@ -1582,7 +2384,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              adapter.completeServiceTargetLoading();          } -        if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { +        if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {              long duration = Tracer.INSTANCE.endLaunchToShortcutTrace();              if (duration >= 0) {                  Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); @@ -1597,7 +2399,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (mResolverDrawerLayout == null) {              return;          } -        int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header; +        int elevatedViewResId = hasWorkProfile() ? +                com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;          final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);          final float defaultElevation = elevatedView.getElevation();          final float chooserHeaderScrollElevation = @@ -1635,7 +2438,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      }      private void maybeSetupGlobalLayoutListener() { -        if (shouldShowTabs()) { +        if (hasWorkProfile()) {              return;          }          final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); @@ -1669,9 +2472,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          if (!shouldShowContentPreview()) {              return false;          } -        boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle( +        boolean isEmpty = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(                  UserHandle.of(UserHandle.myUserId())).getCount() == 0; -        return (mFeatureFlags.scrollablePreview() || shouldShowTabs()) +        return (mFeatureFlags.scrollablePreview() || hasWorkProfile())                  && (!isEmpty || shouldShowContentPreviewWhenEmpty());      } @@ -1690,7 +2493,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements       * @return true if we want to show the content preview area       */      protected boolean shouldShowContentPreview() { -        ChooserRequestParameters chooserRequest = getChooserRequest(); +        ChooserRequest chooserRequest = mViewModel.getChooserRequest();          return (chooserRequest != null) && chooserRequest.isSendActionTarget();      } @@ -1735,34 +2538,22 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          contentPreviewContainer.setVisibility(View.GONE);      } -    private View findRootView() { -        if (mContentView == null) { -            mContentView = findViewById(android.R.id.content); -        } -        return mContentView; -    } - -    /** -     * Intentionally override the {@link ResolverActivity} implementation as we only need that -     * implementation for the intent resolver case. -     */ -    @Override -    public void onButtonClick(View v) {} - -    /** -     * Intentionally override the {@link ResolverActivity} implementation as we only need that -     * implementation for the intent resolver case. -     */ -    @Override -    protected void resetButtonBar() {} - -    @Override      protected String getMetricsCategory() {          return METRICS_CATEGORY_CHOOSER;      } -    @Override -    protected void onProfileTabSelected() { +    protected void onProfileTabSelected(int currentPage) { +        setupViewVisibilities(); +        maybeLogProfileChange(); +        if (hasWorkProfile()) { +            // The device policy logger is only concerned with sessions that include a work profile. +            DevicePolicyEventLogger +                    .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) +                    .setInt(currentPage) +                    .setStrings(getMetricsCategory()) +                    .write(); +        } +          // This fixes an edge case where after performing a variety of gestures, vertical scrolling          // ends up disabled. That's because at some point the old tab's vertical scrolling is          // disabled and the new tab's is enabled. For context, see b/159997845 @@ -1772,14 +2563,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          }      } -    @Override      protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { -        if (shouldShowTabs()) { +        if (hasWorkProfile()) {              mChooserMultiProfilePagerAdapter                      .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());          } -        WindowInsets result = super.onApplyWindowInsets(v, insets); +        WindowInsets result = super_onApplyWindowInsets(v, insets);          if (mResolverDrawerLayout != null) {              mResolverDrawerLayout.requestLayout();          } @@ -1798,7 +2588,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          layoutManager.setVerticalScrollEnabled(enabled);      } -    @Override      void onHorizontalSwipeStateChanged(int state) {          if (state == ViewPager.SCROLL_STATE_DRAGGING) {              if (mScrollStatus == SCROLL_STATUS_IDLE) { @@ -1813,7 +2602,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          }      } -    @Override      protected void maybeLogProfileChange() {          getEventLog().logSharesheetProfileChanged();      } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt index 7bc39a24..84b7d9a9 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt @@ -1,87 +1,23 @@  package com.android.intentresolver.v2 -import android.app.Activity -import android.content.Intent -import android.util.Log  import androidx.activity.ComponentActivity  import androidx.annotation.OpenForTesting -import com.android.intentresolver.ChooserRequestParameters -import com.android.intentresolver.R -import com.android.intentresolver.icons.TargetDataLoader -import com.android.intentresolver.v2.util.mutableLazy - -private const val TAG = "ChooserActivityLogic"  /**   * Activity logic for [ChooserActivity].   *   * TODO: Make this class no longer open once [ChooserActivity] no longer needs to cast to access - *   [chooserRequestParameters]. For now, this class being open is better than using reflection - *   there. + *   [chooserRequest]. For now, this class being open is better than using reflection there.   */  @OpenForTesting  open class ChooserActivityLogic(      tag: String, -    activityProvider: () -> ComponentActivity, +    activity: ComponentActivity,      onWorkProfileStatusUpdated: () -> Unit, -    targetDataLoaderProvider: () -> TargetDataLoader, -    private val onPreInitialization: () -> Unit,  ) :      ActivityLogic,      CommonActivityLogic by CommonActivityLogicImpl(          tag, -        activityProvider, +        activity,          onWorkProfileStatusUpdated, -    ) { - -    override val targetIntent: Intent by lazy { chooserRequestParameters?.targetIntent ?: Intent() } - -    override val resolvingHome: Boolean = false - -    override val title: CharSequence? by lazy { chooserRequestParameters?.title } - -    override val defaultTitleResId: Int by lazy { -        chooserRequestParameters?.defaultTitleResource ?: 0 -    } - -    override val initialIntents: List<Intent>? by lazy { -        chooserRequestParameters?.initialIntents?.toList() -    } - -    override val supportsAlwaysUseOption: Boolean = false - -    override val targetDataLoader: TargetDataLoader by lazy { targetDataLoaderProvider() } - -    override val themeResId: Int = R.style.Theme_DeviceDefault_Chooser - -    private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) } -    override val profileSwitchMessage: String? by _profileSwitchMessage - -    override val payloadIntents: List<Intent> by lazy { -        buildList { -            add(targetIntent) -            chooserRequestParameters?.additionalTargets?.let { addAll(it) } -        } -    } - -    val chooserRequestParameters: ChooserRequestParameters? by lazy { -        try { -            ChooserRequestParameters( -                (activity as Activity).intent, -                referrerPackageName, -                (activity as Activity).referrer, -            ) -        } catch (e: IllegalArgumentException) { -            Log.e(tag, "Caller provided invalid Chooser request parameters", e) -            null -        } -    } - -    override fun preInitialization() { -        onPreInitialization() -    } - -    override fun clearProfileSwitchMessage() { -        _profileSwitchMessage.setLazy(null) -    } -} +    ) diff --git a/java/src/com/android/intentresolver/v2/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt new file mode 100644 index 00000000..17bc2731 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2 + +import android.app.Activity +import androidx.activity.ComponentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import dagger.hilt.android.scopes.ActivityScoped +import javax.inject.Inject + +/** + * __Purpose__ + * + * Cleanup aid. Provides a pathway to cleaner code. + * + * __Incoming References__ + * + * For use by ChooserActivity only; must not be accessed by any code outside of ChooserActivity. + * This prevents circular dependencies and coupling, and maintains unidirectional flow. This is + * important for maintaining a migration path towards healthier architecture. + * + * __Outgoing References__ + * + * _ChooserActivity_ + * + * This class must only reference it's host as Activity/ComponentActivity; no down-cast to + * [ChooserActivity]. Other components should be passed in and not pulled from other places. This + * prevents circular dependencies from forming. + * + * _Elsewhere_ + * + * Where possible, Singleton and ActivityScoped dependencies should be injected here instead of + * referenced from an existing location. If not available for injection, the value should be + * constructed here, then provided to where it is needed. If existing objects from ChooserActivity + * are required, supply a factory interface which satisfies the necessary dependencies and use it + * during construction. + */ + +@ActivityScoped +class ChooserHelper @Inject constructor( +    hostActivity: Activity, +) : DefaultLifecycleObserver { +    // This is guaranteed by Hilt, since only a ComponentActivity is injectable. +    private val activity: ComponentActivity = hostActivity as ComponentActivity + +    private var activityPostCreate: Runnable? = null + +    init { +        activity.lifecycle.addObserver(this) +    } + +    /** +     * Provides a optional callback to setup state which is not yet possible to do without circular +     * dependencies or by moving more code. +     */ +    fun setPostCreateCallback(onPostCreate: Runnable) { +        activityPostCreate = onPostCreate +    } + +    /** +     * Invoked by Lifecycle, after Activity.onCreate() _returns_. +     */ +    override fun onCreate(owner: LifecycleOwner) { +        activityPostCreate?.run() +    } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/v2/ChooserListController.java b/java/src/com/android/intentresolver/v2/ChooserListController.java new file mode 100644 index 00000000..467f343b --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserListController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.model.AbstractResolverComparator; + +import java.util.List; + +public class ChooserListController extends ResolverListController { +    private final List<ComponentName> mFilteredComponents; +    private final SharedPreferences mPinnedComponents; + +    public ChooserListController( +            Context context, +            PackageManager pm, +            Intent targetIntent, +            String referrerPackageName, +            int launchedFromUid, +            AbstractResolverComparator resolverComparator, +            UserHandle queryIntentsAsUser, +            List<ComponentName> filteredComponents, +            SharedPreferences pinnedComponents) { +        super( +                context, +                pm, +                targetIntent, +                referrerPackageName, +                launchedFromUid, +                resolverComparator, +                queryIntentsAsUser); +        mFilteredComponents = filteredComponents; +        mPinnedComponents = pinnedComponents; +    } + +    @Override +    public boolean isComponentFiltered(ComponentName name) { +        return mFilteredComponents.contains(name); +    } + +    @Override +    public boolean isComponentPinned(ComponentName name) { +        return mPinnedComponents.getBoolean(name.flattenToString(), false); +    } +} diff --git a/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt b/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt new file mode 100644 index 00000000..2f8ccf77 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserMutableActionFactory.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2 + +import android.service.chooser.ChooserAction +import com.android.intentresolver.contentpreview.ChooserContentPreviewUi +import com.android.intentresolver.contentpreview.MutableActionFactory +import com.android.intentresolver.widget.ActionRow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +/** A wrapper around [ChooserActionFactory] that provides observable custom actions */ +class ChooserMutableActionFactory( +    private val actionFactory: ChooserActionFactory, +) : MutableActionFactory, ChooserContentPreviewUi.ActionFactory by actionFactory { +    private val customActions = +        MutableStateFlow<List<ActionRow.Action>>(actionFactory.createCustomActions()) + +    override val customActionsFlow: Flow<List<ActionRow.Action>> +        get() = customActions + +    override fun updateCustomActions(actions: List<ChooserAction>) { +        customActions.tryEmit(mapChooserActions(actions)) +    } + +    override fun createCustomActions(): List<ActionRow.Action> = customActions.value + +    private fun mapChooserActions(chooserActions: List<ChooserAction>): List<ActionRow.Action> = +        buildList(chooserActions.size) { +            chooserActions.forEachIndexed { i, chooserAction -> +                val actionRow = +                    actionFactory.createCustomAction(chooserAction) { +                        actionFactory.logCustomAction(i) +                    } +                if (actionRow != null) { +                    add(actionRow) +                } +            } +        } +} diff --git a/java/src/com/android/intentresolver/v2/IntentForwarding.kt b/java/src/com/android/intentresolver/v2/IntentForwarding.kt new file mode 100644 index 00000000..3d366d10 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/IntentForwarding.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2 + +import android.Manifest +import android.Manifest.permission.INTERACT_ACROSS_USERS +import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.PermissionChecker +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import com.android.intentresolver.v2.data.repository.DevicePolicyResources +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG: String = "IntentForwarding" + +@Singleton +class IntentForwarding +@Inject +constructor( +    private val resources: DevicePolicyResources, +    private val userManager: UserManager, +    private val packageManager: PackageManager +) { + +    fun forwardMessageFor(intent: Intent): String? { +        val contentUserHint = intent.contentUserHint +        if ( +            contentUserHint != UserHandle.USER_CURRENT && contentUserHint != UserHandle.myUserId() +        ) { +            val originUserInfo = userManager.getUserInfo(contentUserHint) +            val originIsManaged = originUserInfo?.isManagedProfile ?: false +            val targetIsManaged = userManager.isManagedProfile +            return when { +                originIsManaged && !targetIsManaged -> resources.forwardToPersonalMessage +                !originIsManaged && targetIsManaged -> resources.forwardToWorkMessage +                else -> null +            } +        } +        return null +    } + +    private fun isPermissionGranted(permission: String, uid: Int) = +        ActivityManager.checkComponentPermission( +            /* permission = */ permission, +            /* uid = */ uid, +            /* owningUid= */ -1, +            /* exported= */ true +        ) + +    /** +     * Returns whether the package has the necessary permissions to interact across profiles on +     * behalf of a given user. +     * +     * This means meeting the following condition: +     * * The app's [ApplicationInfo.crossProfile] flag must be true, and at least one of the +     *   following conditions must be fulfilled +     * * `Manifest.permission.INTERACT_ACROSS_USERS_FULL` granted. +     * * `Manifest.permission.INTERACT_ACROSS_USERS` granted. +     * * `Manifest.permission.INTERACT_ACROSS_PROFILES` granted, or the corresponding AppOps +     *   `android:interact_across_profiles` is set to "allow". +     */ +    fun canAppInteractAcrossProfiles(context: Context, packageName: String): Boolean { +        val applicationInfo: ApplicationInfo +        try { +            applicationInfo = packageManager.getApplicationInfo(packageName, 0) +        } catch (e: PackageManager.NameNotFoundException) { +            Log.e(TAG, "Package $packageName does not exist on current user.") +            return false +        } +        if (!applicationInfo.crossProfile) { +            return false +        } + +        val packageUid = applicationInfo.uid + +        if (isPermissionGranted(INTERACT_ACROSS_USERS_FULL, packageUid) == PERMISSION_GRANTED) { +            return true +        } +        if (isPermissionGranted(INTERACT_ACROSS_USERS, packageUid) == PERMISSION_GRANTED) { +            return true +        } +        return PermissionChecker.checkPermissionForPreflight( +            context, +            Manifest.permission.INTERACT_ACROSS_PROFILES, +            PermissionChecker.PID_UNKNOWN, +            packageUid, +            packageName +        ) == PERMISSION_GRANTED +    } +} diff --git a/java/src/com/android/intentresolver/v2/JavaFlowHelper.kt b/java/src/com/android/intentresolver/v2/JavaFlowHelper.kt new file mode 100644 index 00000000..c6c977f6 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/JavaFlowHelper.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("JavaFlowHelper") + +package com.android.intentresolver.v2 + +import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +fun <T> collect(scope: CoroutineScope, flow: Flow<T>, collector: Consumer<T>): Job = +    scope.launch { flow.collect { collector.accept(it) } } diff --git a/java/src/com/android/intentresolver/v2/ProfileAvailability.kt b/java/src/com/android/intentresolver/v2/ProfileAvailability.kt new file mode 100644 index 00000000..4d689724 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ProfileAvailability.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2 + +import com.android.intentresolver.v2.domain.interactor.UserInteractor +import com.android.intentresolver.v2.shared.model.Profile +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout + +/** Provides availability status for profiles */ +class ProfileAvailability( +    private val scope: CoroutineScope, +    private val userInteractor: UserInteractor +) { +    private val availability = +        userInteractor.availability.stateIn(scope, SharingStarted.Eagerly, mapOf()) + +    /** Used by WorkProfilePausedEmptyStateProvider */ +    var waitingToEnableProfile = false +        private set + +    private var waitJob: Job? = null +    /** Query current profile availability. An unavailable profile is one which is not active. */ +    fun isAvailable(profile: Profile) = availability.value[profile] ?: false + +    /** Used by WorkProfilePausedEmptyStateProvider */ +    fun requestQuietModeState(profile: Profile, quietMode: Boolean) { +        val enableProfile = !quietMode + +        // Check if the profile is already in the correct state +        if (isAvailable(profile) == enableProfile) { +            return // No-op +        } + +        // Support existing code +        if (enableProfile) { +            waitingToEnableProfile = true +            waitJob?.cancel() + +            val job = scope.launch { +                // Wait for the profile to become available +                // Wait for the profile to be enabled, then clear this flag +                userInteractor.availability.filter { it[profile] == true }.first() +                waitingToEnableProfile = false +            } +            job.invokeOnCompletion { +                waitingToEnableProfile = false +            } +            waitJob = job +        } + +        // Apply the change +        scope.launch { userInteractor.updateState(profile, enableProfile) } +    } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/v2/ProfileHelper.kt b/java/src/com/android/intentresolver/v2/ProfileHelper.kt new file mode 100644 index 00000000..784096b4 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ProfileHelper.kt @@ -0,0 +1,74 @@ +/* +* Copyright (C) 2024 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.android.intentresolver.v2 + +import android.os.UserHandle +import com.android.intentresolver.inject.IntentResolverFlags +import com.android.intentresolver.v2.domain.interactor.UserInteractor +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.shared.model.User +import javax.inject.Inject + +class ProfileHelper @Inject constructor( +    interactor: UserInteractor, +    private val flags: IntentResolverFlags, +    profiles: List<Profile>, +    launchedAsProfile: Profile, +) { +    private val launchedByHandle: UserHandle = interactor.launchedAs + +    // Map UserHandle back to a user within launchedByProfile +    private val launchedByUser = when (launchedByHandle) { +        launchedAsProfile.primary.handle -> launchedAsProfile.primary +        launchedAsProfile.clone?.handle -> launchedAsProfile.clone +        else -> error("launchedByUser must be a member of launchedByProfile") +    } +    val launchedAsProfileType: Profile.Type = launchedAsProfile.type + +    val personalProfile = profiles.single { it.type == Profile.Type.PERSONAL } +    val workProfile = profiles.singleOrNull { it.type == Profile.Type.WORK } +    val privateProfile = profiles.singleOrNull { it.type == Profile.Type.PRIVATE } + +    val personalHandle = personalProfile.primary.handle +    val workHandle = workProfile?.primary?.handle +    val privateHandle = privateProfile?.primary?.handle?.takeIf { flags.enablePrivateProfile() } +    val cloneHandle = personalProfile.clone?.handle + +    val isLaunchedAsCloneProfile = launchedByUser == launchedAsProfile.clone + +    val cloneUserPresent = personalProfile.clone != null +    val workProfilePresent = workProfile != null +    val privateProfilePresent = privateProfile != null + +    // Name retained for ease of review, to be renamed later +    val tabOwnerUserHandleForLaunch = if (launchedByUser.role == User.Role.CLONE) { +        // When started by clone user, return the profile owner instead +        launchedAsProfile.primary.handle +    } else { +        // Otherwise the launched user is used +        launchedByUser.handle +    } + +    // Name retained for ease of review, to be renamed later +    fun getQueryIntentsHandle(handle: UserHandle): UserHandle? { +        return if (isLaunchedAsCloneProfile && handle == personalHandle) { +            cloneHandle +        } else { +            handle +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 2ba50ec3..0182fc89 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -16,34 +16,29 @@  package com.android.intentresolver.v2; -import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;  import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static android.content.PermissionChecker.PID_UNKNOWN;  import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;  import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;  import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs; +import static com.android.intentresolver.v2.ui.viewmodel.ResolverRequestReaderKt.readResolverRequest;  import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; -import static java.util.Collections.emptyList;  import static java.util.Objects.requireNonNull; -import static java.util.Objects.requireNonNullElse; -import android.app.ActivityManager;  import android.app.ActivityThread;  import android.app.VoiceInteractor.PickOptionRequest;  import android.app.VoiceInteractor.PickOptionRequest.Option;  import android.app.VoiceInteractor.Prompt;  import android.app.admin.DevicePolicyEventLogger; -import android.app.admin.DevicePolicyManager;  import android.content.ComponentName;  import android.content.Context;  import android.content.Intent;  import android.content.IntentFilter; -import android.content.PermissionChecker;  import android.content.pm.ActivityInfo;  import android.content.pm.ApplicationInfo;  import android.content.pm.PackageManager; @@ -51,7 +46,6 @@ import android.content.pm.PackageManager.NameNotFoundException;  import android.content.pm.ResolveInfo;  import android.content.pm.UserInfo;  import android.content.res.Configuration; -import android.content.res.TypedArray;  import android.graphics.Insets;  import android.net.Uri;  import android.os.Build; @@ -83,15 +77,13 @@ import android.widget.ImageView;  import android.widget.ListView;  import android.widget.Space;  import android.widget.TabHost; -import android.widget.TabWidget;  import android.widget.TextView;  import android.widget.Toast;  import androidx.annotation.NonNull;  import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.annotation.UiThread;  import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.viewmodel.CreationExtras;  import androidx.viewpager.widget.ViewPager;  import com.android.intentresolver.AnnotatedUserHandles; @@ -105,24 +97,40 @@ import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;  import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;  import com.android.intentresolver.emptystate.EmptyState;  import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.icons.DefaultTargetDataLoader;  import com.android.intentresolver.icons.TargetDataLoader;  import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.Profile;  import com.android.intentresolver.v2.data.repository.DevicePolicyResources;  import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider;  import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;  import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; -import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.emptystate.ResolverWorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.ResolverMultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.TabConfig; +import com.android.intentresolver.v2.shared.model.Profile;  import com.android.intentresolver.v2.ui.ActionTitle; +import com.android.intentresolver.v2.ui.model.ActivityModel; +import com.android.intentresolver.v2.ui.model.ResolverRequest; +import com.android.intentresolver.v2.validation.Finding; +import com.android.intentresolver.v2.validation.FindingsKt; +import com.android.intentresolver.v2.validation.Invalid; +import com.android.intentresolver.v2.validation.Valid; +import com.android.intentresolver.v2.validation.ValidationResult;  import com.android.intentresolver.widget.ResolverDrawerLayout;  import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.content.PackageMonitor;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.util.LatencyTracker; +import com.google.common.collect.ImmutableList; + +import dagger.hilt.android.AndroidEntryPoint; + +import kotlin.Pair;  import kotlin.Unit;  import java.util.ArrayList; @@ -131,6 +139,9 @@ import java.util.Iterator;  import java.util.List;  import java.util.Objects;  import java.util.Set; +import java.util.function.Consumer; + +import javax.inject.Inject;  /**   * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is @@ -138,23 +149,18 @@ import java.util.Set;   * frameworks/base/core/java/com/android/internal/app/ResolverActivity.java for that), the full   * migration is not complete.   */ -@UiThread -public class ResolverActivity extends FragmentActivity implements +@AndroidEntryPoint(FragmentActivity.class) +public class ResolverActivity extends Hilt_ResolverActivity implements          ResolverListAdapter.ResolverListCommunicator { -    private final List<Runnable> mInit = new ArrayList<>(); - +    @Inject public PackageManager mPackageManager; +    @Inject public DevicePolicyResources mDevicePolicyResources; +    @Inject public IntentForwarding mIntentForwarding; +    private ResolverRequest mResolverRequest; +    private ActivityModel mActivityModel;      protected ActivityLogic mLogic; - -    private DevicePolicyResources mDevicePolicyResources; - -    public ResolverActivity() { -        mIsIntentPicker = getClass().equals(ResolverActivity.class); -    } - -    protected ResolverActivity(boolean isIntentPicker) { -        mIsIntentPicker = isIntentPicker; -    } +    protected TargetDataLoader mTargetDataLoader; +    private boolean mResolvingHome;      private Button mAlwaysButton;      private Button mOnceButton; @@ -163,9 +169,7 @@ public class ResolverActivity extends FragmentActivity implements      private int mLayoutId;      private PickTargetOptionRequest mPickOptionRequest;      // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity. -    private final boolean mIsIntentPicker;      protected ResolverDrawerLayout mResolverDrawerLayout; -    protected PackageManager mPm;      private static final String TAG = "ResolverActivity";      private static final boolean DEBUG = false; @@ -176,64 +180,33 @@ public class ResolverActivity extends FragmentActivity implements      protected Insets mSystemWindowInsets = null;      private Space mFooterSpacer = null; -    /** See {@link #setRetainInOnStop}. */ -    private boolean mRetainInOnStop; -      protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";      protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";      /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */ -    private boolean mWorkProfileHasBeenEnabled = false; +    private final boolean mWorkProfileHasBeenEnabled = false; -    private static final String TAB_TAG_PERSONAL = "personal"; -    private static final String TAB_TAG_WORK = "work"; +    protected static final String TAB_TAG_PERSONAL = "personal"; +    protected static final String TAB_TAG_WORK = "work";      private PackageMonitor mPersonalPackageMonitor;      private PackageMonitor mWorkPackageMonitor; -    @VisibleForTesting -    protected MultiProfilePagerAdapter mMultiProfilePagerAdapter; - +    protected ResolverMultiProfilePagerAdapter mMultiProfilePagerAdapter; -    // Intent extra for connected audio devices -    public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"; - -    /** -     * Integer extra to indicate which profile should be automatically selected. -     * <p>Can only be used if there is a work profile. -     * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. -     */ -    protected static final String EXTRA_SELECTED_PROFILE = -            "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE"; - -    /** -     * {@link UserHandle} extra to indicate the user of the user that the starting intent -     * originated from. -     * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()}, -     * as there are edge cases when the intent resolver is launched in the other profile. -     * For example, when we have 0 resolved apps in current profile and multiple resolved -     * apps in the other profile, opening a link from the current profile launches the intent -     * resolver in the other one. b/148536209 for more info. -     */ -    static final String EXTRA_CALLING_USER = -            "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; - -    protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; -    protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; +    public static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; +    public static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;      private UserHandle mHeaderCreatorUser;      @Nullable      private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; -    protected final LatencyTracker mLatencyTracker = getLatencyTracker(); -      protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {          return new PackageMonitor() {              @Override              public void onSomePackagesChanged() {                  listAdapter.handlePackagesChanged(); -                updateProfileViewButton();              }              @Override @@ -244,65 +217,63 @@ public class ResolverActivity extends FragmentActivity implements              }          };      } -    protected interface Initializer { -        void initialize(ActivityLogic value); +    protected ActivityModel createActivityModel() { +        return ActivityModel.createFrom(this);      } -    protected void setLogic(ActivityLogic logic) { -        mLogic = logic; +    @VisibleForTesting +    protected ActivityLogic createActivityLogic() { +        return  new ResolverActivityLogic( +                TAG, +                /* activity = */ this, +                this::onWorkProfileStatusUpdated);      } -    protected void addInitializer(Runnable initializer) { -        mInit.add(initializer); +    @NonNull +    @Override +    public CreationExtras getDefaultViewModelCreationExtras() { +        return addDefaultArgs( +                super.getDefaultViewModelCreationExtras(), +                new Pair<>(ActivityModel.ACTIVITY_MODEL_KEY, ActivityModel.createFrom(this)));      }      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState); -        if (isFinishing()) { -            // Performing a clean exit: -            //    Skip initializing anything. -            return; +        setTheme(R.style.Theme_DeviceDefault_Resolver); +        mActivityModel = createActivityModel(); + +        Log.i(TAG, "onCreate"); +        Log.i(TAG, "activityModel=" + mActivityModel.toString()); +        int callerUid = mActivityModel.getLaunchedFromUid(); +        if (callerUid < 0 || UserHandle.isIsolated(callerUid)) { +            Log.e(TAG, "Can't start a resolver from uid " + callerUid); +            finish();          } -        mDevicePolicyResources = new DevicePolicyResources(getApplication().getResources(), -                requireNonNull(getSystemService(DevicePolicyManager.class))); -        setLogic(new ResolverActivityLogic( -                TAG, -                () -> this, -                this::onWorkProfileStatusUpdated)); -        addInitializer(this::init); -    } -    @Override -    protected final void onPostCreate(@Nullable Bundle savedInstanceState) { -        super.onPostCreate(savedInstanceState); -        mInit.forEach(Runnable::run); - -        if (savedInstanceState != null) { -            resetButtonBar(); -            ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); -            if (viewPager != null) { -                viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); -            } -            mMultiProfilePagerAdapter.clearInactiveProfileCache(); +        ValidationResult<ResolverRequest> result = readResolverRequest(mActivityModel); +        if (result instanceof Invalid) { +            ((Invalid) result).getErrors().forEach(new Consumer<Finding>() { +                @Override +                public void accept(Finding finding) { +                    FindingsKt.log(finding, TAG); +                } +            }); +            finish();          } +        mResolverRequest = ((Valid<ResolverRequest>) result).getValue(); +        mLogic = createActivityLogic(); +        mResolvingHome = mResolverRequest.isResolvingHome(); +        mTargetDataLoader = new DefaultTargetDataLoader( +                this, +                getLifecycle(), +                mResolverRequest.isAudioCaptureDevice()); +        init(); +        restore(savedInstanceState);      }      private void init() { -        setTheme(mLogic.getThemeResId()); -        mLogic.preInitialization(); - -        Intent intent = mLogic.getTargetIntent(); -        List<Intent> initialIntents = mLogic.getInitialIntents(); -        TargetDataLoader targetDataLoader = mLogic.getTargetDataLoader(); - -        // Calling UID did not have valid permissions -        if (mLogic.getAnnotatedUserHandles() == null) { -            finish(); -            return; -        } - -        mPm = getPackageManager(); +        Intent intent = mResolverRequest.getIntent();          // The last argument of createResolverListAdapter is whether to do special handling          // of the last used choice to highlight it in the list.  We need to always @@ -312,15 +283,14 @@ public class ResolverActivity extends FragmentActivity implements          // We also turn it off when clonedProfile is present on the device, because we might have          // different "last chosen" activities in the different profiles, and PackageManager doesn't          // provide any more information to help us select between them. -        boolean filterLastUsed = mLogic.getSupportsAlwaysUseOption() && !isVoiceInteraction() -                && !shouldShowTabs() && !hasCloneProfile(); +        boolean filterLastUsed = !isVoiceInteraction() +                && !hasWorkProfile() && !hasCloneProfile();          mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( -                requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), -                /* resolutionList = */ null, -                filterLastUsed, -                targetDataLoader +                new Intent[0], +                /* resolutionList = */ mResolverRequest.getResolutionList(), +                filterLastUsed          ); -        if (configureContentView(targetDataLoader)) { +        if (configureContentView(mTargetDataLoader)) {              return;          } @@ -354,7 +324,7 @@ public class ResolverActivity extends FragmentActivity implements                  }              }); -            boolean hasTouchScreen = getPackageManager() +            boolean hasTouchScreen = mPackageManager                      .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);              if (isVoiceInteraction() || !hasTouchScreen) { @@ -368,12 +338,6 @@ public class ResolverActivity extends FragmentActivity implements              mResolverDrawerLayout = rdl;          } -        mProfileView = findViewById(com.android.internal.R.id.profile_button); -        if (mProfileView != null) { -            mProfileView.setOnClickListener(this::onProfileClick); -            updateProfileViewButton(); -        } -          final Set<String> categories = intent.getCategories();          MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()                  ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED @@ -382,19 +346,31 @@ public class ResolverActivity extends FragmentActivity implements                          + (categories != null ? Arrays.toString(categories.toArray()) : ""));      } -    protected MultiProfilePagerAdapter createMultiProfilePagerAdapter( +    private void restore(@Nullable Bundle savedInstanceState) { +        if (savedInstanceState != null) { +            // onRestoreInstanceState +            resetButtonBar(); +            ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); +            if (viewPager != null) { +                viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); +            } +        } + +        mMultiProfilePagerAdapter.clearInactiveProfileCache(); +    } + +    protected ResolverMultiProfilePagerAdapter createMultiProfilePagerAdapter(              Intent[] initialIntents,              List<ResolveInfo> resolutionList, -            boolean filterLastUsed, -            TargetDataLoader targetDataLoader) { -        MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; -        if (shouldShowTabs()) { +            boolean filterLastUsed) { +        ResolverMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; +        if (hasWorkProfile()) {              resolverMultiProfilePagerAdapter =                      createResolverMultiProfilePagerAdapterForTwoProfiles( -                            initialIntents, resolutionList, filterLastUsed, targetDataLoader); +                            initialIntents, resolutionList, filterLastUsed);          } else {              resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile( -                    initialIntents, resolutionList, filterLastUsed, targetDataLoader); +                    initialIntents, resolutionList, filterLastUsed);          }          return resolverMultiProfilePagerAdapter;      } @@ -448,9 +424,7 @@ public class ResolverActivity extends FragmentActivity implements          if (useLayoutWithDefault()) return true;          View buttonBar = findViewById(com.android.internal.R.id.button_bar); -        if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true; - -        return false; +        return buttonBar == null || buttonBar.getVisibility() == View.GONE;      }      protected void applyFooterView(int height) { @@ -492,7 +466,7 @@ public class ResolverActivity extends FragmentActivity implements      public void onConfigurationChanged(Configuration newConfig) {          super.onConfigurationChanged(newConfig);          mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); -        if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault() +        if (hasWorkProfile() && !useLayoutWithDefault()                  && !shouldUseMiniResolver()) {              updateIntentPickerPaddings();          } @@ -525,7 +499,7 @@ public class ResolverActivity extends FragmentActivity implements          }          final Intent intent = getIntent();          if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() -                && !mLogic.getResolvingHome() && !mRetainInOnStop) { +                && !mResolvingHome) {              // This resolver is in the unusual situation where it has been              // launched at the top of a new task.  We don't let it be added              // to the recent tasks shown to the user, and we need to make sure @@ -553,6 +527,7 @@ public class ResolverActivity extends FragmentActivity implements          }      } +    // referenced by layout XML: android:onClick="onButtonClick"      public void onButtonClick(View v) {          final int id = v.getId();          ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); @@ -570,8 +545,8 @@ public class ResolverActivity extends FragmentActivity implements          }          ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()                  .resolveInfoForPosition(which, hasIndexBeenFiltered); -        if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) { -            String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString(); +        if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { +            String launcherName = ri.activityInfo.loadLabel(mPackageManager).toString();              Toast.makeText(this,                      mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName),                      Toast.LENGTH_LONG).show(); @@ -584,15 +559,12 @@ public class ResolverActivity extends FragmentActivity implements              return;          }          if (onTargetSelected(target, always)) { -            if (always && mLogic.getSupportsAlwaysUseOption()) { +            if (always) {                  MetricsLogger.action(                          this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS); -            } else if (mLogic.getSupportsAlwaysUseOption()) { -                MetricsLogger.action( -                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);              } else {                  MetricsLogger.action( -                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP); +                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);              }              MetricsLogger.action(this,                      mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() @@ -602,9 +574,6 @@ public class ResolverActivity extends FragmentActivity implements          }      } -    /** -     * Replace me in subclasses! -     */      @Override // ResolverListCommunicator      public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {          return defIntent; @@ -613,7 +582,7 @@ public class ResolverActivity extends FragmentActivity implements      protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {          final ItemClickListener listener = new ItemClickListener();          setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener); -        if (shouldShowTabs() && mIsIntentPicker) { +        if (hasWorkProfile()) {              final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);              if (rdl != null) {                  rdl.setMaxCollapsedHeight(getResources() @@ -628,8 +597,7 @@ public class ResolverActivity extends FragmentActivity implements          final ResolveInfo ri = target.getResolveInfo();          final Intent intent = target != null ? target.getResolvedIntent() : null; -        if (intent != null && (mLogic.getSupportsAlwaysUseOption() -                || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()) +        if (intent != null /*&& mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()*/                  && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {              // Build a reasonable intent filter, based on what matched.              IntentFilter filter = new IntentFilter(); @@ -672,7 +640,7 @@ public class ResolverActivity extends FragmentActivity implements                  // or "content:" schemes (see IntentFilter for the reason).                  if (cat != IntentFilter.MATCH_CATEGORY_TYPE                          || (!"file".equals(data.getScheme()) -                                && !"content".equals(data.getScheme()))) { +                        && !"content".equals(data.getScheme()))) {                      filter.addDataScheme(data.getScheme());                      // Look through the resolved filter to determine which part @@ -730,7 +698,7 @@ public class ResolverActivity extends FragmentActivity implements                  }                  int bestMatch = 0; -                for (int i=0; i<N; i++) { +                for (int i = 0; i < N; i++) {                      ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()                              .getUnfilteredResolveList().get(i).getResolveInfoAt(0);                      set[i] = new ComponentName(r.activityInfo.packageName, @@ -748,7 +716,7 @@ public class ResolverActivity extends FragmentActivity implements                  if (always) {                      final int userId = getUserId(); -                    final PackageManager pm = getPackageManager(); +                    final PackageManager pm = mPackageManager;                      // Set the preferred Activity                      pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent()); @@ -757,7 +725,8 @@ public class ResolverActivity extends FragmentActivity implements                          // Set default Browser if needed                          final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);                          if (TextUtils.isEmpty(packageName)) { -                            pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId); +                            pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, +                                    userId);                          }                      }                  } else { @@ -771,21 +740,11 @@ public class ResolverActivity extends FragmentActivity implements              }          } -        if (target != null) { -            safelyStartActivity(target); - -            // Rely on the ActivityManager to pop up a dialog regarding app suspension -            // and return false -            if (target.isSuspended()) { -                return false; -            } -        } - -        return true; -    } +        safelyStartActivity(target); -    public void onActivityStarted(TargetInfo cti) { -        // Do nothing +        // Rely on the ActivityManager to pop up a dialog regarding app suspension +        // and return false +        return !target.isSuspended();      }      @Override // ResolverListCommunicator @@ -797,34 +756,23 @@ public class ResolverActivity extends FragmentActivity implements          return !target.isSuspended();      } -    // TODO: this method takes an unused `UserHandle` because the override in `ChooserActivity` uses -    // that data to set up other components as dependencies of the controller. In reality, these -    // methods don't require polymorphism, because they're only invoked from within their respective -    // concrete class; `ResolverActivity` will never call this method expecting to get a -    // `ChooserListController` (subclass) result, because `ResolverActivity` only invokes this -    // method as part of handling `createMultiProfilePagerAdapter()`, which is itself overridden in -    // `ChooserActivity`. A future refactoring could better express the coupling between the adapter -    // and controller types; in the meantime, structuring as an override (with matching signatures) -    // shows that these methods are *structurally* related, and helps to prevent any regressions in -    // the future if resolver *were* to make any (non-overridden) calls to a version that used a -    // different signature (and thus didn't return the subclass type).      @VisibleForTesting      protected ResolverListController createListController(UserHandle userHandle) {          ResolverRankerServiceResolverComparator resolverComparator =                  new ResolverRankerServiceResolverComparator(                          this, -                        mLogic.getTargetIntent(), -                        mLogic.getReferrerPackageName(), +                        mResolverRequest.getIntent(), +                        mActivityModel.getReferrerPackage(),                          null,                          null,                          getResolverRankerServiceUserHandleList(userHandle),                          null);          return new ResolverListController(                  this, -                mPm, -                mLogic.getTargetIntent(), -                mLogic.getReferrerPackageName(), -                requireAnnotatedUserHandles().userIdOfCallingApp, +                mPackageManager, +                mActivityModel.getIntent(), +                mActivityModel.getReferrerPackage(), +                mActivityModel.getLaunchedFromUid(),                  resolverComparator,                  getQueryIntentsUser(userHandle));      } @@ -839,13 +787,30 @@ public class ResolverActivity extends FragmentActivity implements          return postRebuildListInternal(rebuildCompleted);      } -    void onHorizontalSwipeStateChanged(int state) {} -      /**       * Callback called when user changes the profile tab. -     * <p>This method is intended to be overridden by subclasses.       */ -    protected void onProfileTabSelected() { } +    /* TODO: consider merging with the customized considerations of our implemented +     * {@link MultiProfilePagerAdapter.OnProfileSelectedListener}. The only apparent distinctions +     * between the respective listener callbacks would occur in the triggering patterns during init +     * (when the `OnProfileSelectedListener` is registered after a possible tab-change), or possibly +     * if there's some way to trigger an update in one model but not the other.  If there's an +     * initialization dependency, we can probably reason about it with confidence. If there's a +     * discrepancy between the `TabHost` and pager-adapter data models, that inconsistency is +     * likely to be a bug that would benefit from consolidation. +     */ +    protected void onProfileTabSelected(int currentPage) { +        setupViewVisibilities(); +        maybeLogProfileChange(); +        if (hasWorkProfile()) { +            // The device policy logger is only concerned with sessions that include a work profile. +            DevicePolicyEventLogger +                    .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) +                    .setInt(currentPage) +                    .setStrings(getMetricsCategory()) +                    .write(); +        } +    }      /**       * Add a label to signify that the user can pick a different app. @@ -858,7 +823,7 @@ public class ResolverActivity extends FragmentActivity implements              stub.setVisibility(View.VISIBLE);              TextView textView = (TextView) LayoutInflater.from(this).inflate(                      R.layout.resolver_different_item_header, null, false); -            if (shouldShowTabs()) { +            if (hasWorkProfile()) {                  textView.setGravity(Gravity.CENTER);              }              stub.addView(textView); @@ -866,9 +831,6 @@ public class ResolverActivity extends FragmentActivity implements      }      protected void resetButtonBar() { -        if (!mLogic.getSupportsAlwaysUseOption()) { -            return; -        }          final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);          if (buttonLayout == null) {              Log.e(TAG, "Layout unexpectedly does not have a button bar"); @@ -921,21 +883,13 @@ public class ResolverActivity extends FragmentActivity implements      protected void maybeLogProfileChange() {} -    // @NonFinalForTesting -    @VisibleForTesting -    protected MyUserIdProvider createMyUserIdProvider() { -        return new MyUserIdProvider(); -    } - -    // @NonFinalForTesting      @VisibleForTesting      protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {          return new CrossProfileIntentsChecker(getContentResolver());      }      protected Unit onWorkProfileStatusUpdated() { -        if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals( -                requireAnnotatedUserHandles().workProfileUserHandle)) { +        if (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_WORK) {              mMultiProfilePagerAdapter.rebuildActiveTab(true);          } else {              mMultiProfilePagerAdapter.clearInactiveProfileCache(); @@ -951,8 +905,7 @@ public class ResolverActivity extends FragmentActivity implements              Intent[] initialIntents,              List<ResolveInfo> resolutionList,              boolean filterLastUsed, -            UserHandle userHandle, -            TargetDataLoader targetDataLoader) { +            UserHandle userHandle) {          UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()                  && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)                  ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle; @@ -964,26 +917,10 @@ public class ResolverActivity extends FragmentActivity implements                  filterLastUsed,                  createListController(userHandle),                  userHandle, -                mLogic.getTargetIntent(), +                mResolverRequest.getIntent(),                  this,                  initialIntentsUserSpace, -                targetDataLoader); -    } - -    private LatencyTracker getLatencyTracker() { -        return LatencyTracker.getInstance(this); -    } - -    /** -     * Get the string resource to be used as a label for the link to the resolver activity for an -     * action. -     * -     * @param action The action to resolve -     * -     * @return The string resource to be used as a label -     */ -    public static @StringRes int getLabelRes(String action) { -        return ActionTitle.forAction(action).labelRes; +                mTargetDataLoader);      }      protected final EmptyStateProvider createEmptyStateProvider( @@ -991,7 +928,7 @@ public class ResolverActivity extends FragmentActivity implements          final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();          final EmptyStateProvider workProfileOffEmptyStateProvider = -                new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle, +                new ResolverWorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,                          mLogic.getWorkProfileAvailabilityManager(),                          /* onSwitchOnWorkSelectedListener= */                          () -> { @@ -1021,36 +958,40 @@ public class ResolverActivity extends FragmentActivity implements              createResolverMultiProfilePagerAdapterForOneProfile(                      Intent[] initialIntents,                      List<ResolveInfo> resolutionList, -                    boolean filterLastUsed, -                    TargetDataLoader targetDataLoader) { -        ResolverListAdapter adapter = createResolverListAdapter( +                    boolean filterLastUsed) { +        ResolverListAdapter personalAdapter = createResolverListAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mResolverRequest.getPayloadIntents(),                  initialIntents,                  resolutionList,                  filterLastUsed, -                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, -                targetDataLoader); +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle +        );          return new ResolverMultiProfilePagerAdapter(                  /* context */ this, -                adapter, +                ImmutableList.of( +                        new TabConfig<>( +                                PROFILE_PERSONAL, +                                mDevicePolicyResources.getPersonalTabLabel(), +                                mDevicePolicyResources.getPersonalTabAccessibilityLabel(), +                                TAB_TAG_PERSONAL, +                                personalAdapter)),                  createEmptyStateProvider(/* workProfileUserHandle= */ null),                  /* workProfileQuietModeChecker= */ () -> false, +                /* defaultProfile= */ PROFILE_PERSONAL,                  /* workProfileUserHandle= */ null,                  requireAnnotatedUserHandles().cloneProfileUserHandle);      }      private UserHandle getIntentUser() { -        return getIntent().hasExtra(EXTRA_CALLING_USER) -                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) -                : requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch; +        return Objects.requireNonNullElse(mResolverRequest.getCallingUser(), +                requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);      }      private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(              Intent[] initialIntents,              List<ResolveInfo> resolutionList, -            boolean filterLastUsed, -            TargetDataLoader targetDataLoader) { +            boolean filterLastUsed) {          // In the edge case when we have 0 apps in the current profile and >1 apps in the other,          // the intent resolver is started in the other profile. Since this is the only case when          // this happens, we check for it here and set the current profile's tab. @@ -1073,27 +1014,38 @@ public class ResolverActivity extends FragmentActivity implements          // resolver list. So filterLastUsed should be false for the other profile.          ResolverListAdapter personalAdapter = createResolverListAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mResolverRequest.getPayloadIntents(),                  selectedProfile == PROFILE_PERSONAL ? initialIntents : null,                  resolutionList,                  (filterLastUsed && UserHandle.myUserId()                          == requireAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()), -                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, -                targetDataLoader); +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle +        );          UserHandle workProfileUserHandle = requireAnnotatedUserHandles().workProfileUserHandle;          ResolverListAdapter workAdapter = createResolverListAdapter(                  /* context */ this, -                mLogic.getPayloadIntents(), +                mResolverRequest.getPayloadIntents(),                  selectedProfile == PROFILE_WORK ? initialIntents : null,                  resolutionList,                  (filterLastUsed && UserHandle.myUserId()                          == workProfileUserHandle.getIdentifier()), -                /* userHandle */ workProfileUserHandle, -                targetDataLoader); +                /* userHandle */ workProfileUserHandle +        );          return new ResolverMultiProfilePagerAdapter(                  /* context */ this, -                personalAdapter, -                workAdapter, +                ImmutableList.of( +                        new TabConfig<>( +                                PROFILE_PERSONAL, +                                mDevicePolicyResources.getPersonalTabLabel(), +                                mDevicePolicyResources.getPersonalTabAccessibilityLabel(), +                                TAB_TAG_PERSONAL, +                                personalAdapter), +                        new TabConfig<>( +                                PROFILE_WORK, +                                mDevicePolicyResources.getWorkTabLabel(), +                                mDevicePolicyResources.getWorkTabAccessibilityLabel(), +                                TAB_TAG_WORK, +                                workAdapter)),                  createEmptyStateProvider(workProfileUserHandle),                  () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),                  selectedProfile, @@ -1104,23 +1056,20 @@ public class ResolverActivity extends FragmentActivity implements      /**       * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link       * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied. -     * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE} -     * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}       */      final int getSelectedProfileExtra() { -        int selectedProfile = -1; -        if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) { -            selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1); -            if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) { -                throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value " -                        + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or " -                        + "ResolverActivity.PROFILE_WORK."); -            } +        Profile.Type selected = mResolverRequest.getSelectedProfile(); +        if (selected == null) { +            return -1; +        } +        switch (selected) { +            case PERSONAL: return PROFILE_PERSONAL; +            case WORK: return PROFILE_WORK; +            default: return -1;          } -        return selectedProfile;      } -    protected final @Profile int getCurrentProfile() { +    protected final @ProfileType int getCurrentProfile() {          UserHandle launchUser = requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch;          UserHandle personalUser = requireAnnotatedUserHandles().personalProfileUserHandle;          return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK; @@ -1144,24 +1093,6 @@ public class ResolverActivity extends FragmentActivity implements          return hasCloneProfile() && launchUser.equals(cloneUser);      } -    protected final boolean shouldShowTabs() { -        return hasWorkProfile(); -    } - -    protected final void onProfileClick(View v) { -        final DisplayResolveInfo dri = -                mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile(); -        if (dri == null) { -            return; -        } - -        // Do not show the profile switch message anymore. -        mLogic.clearProfileSwitchMessage(); - -        onTargetSelected(dri, false); -        finish(); -    } -      private void updateIntentPickerPaddings() {          View titleCont = findViewById(com.android.internal.R.id.title_container);          titleCont.setPadding( @@ -1219,28 +1150,8 @@ public class ResolverActivity extends FragmentActivity implements          return new Option(getOrLoadDisplayLabel(target), index);      } -    @Override // ResolverListCommunicator -    public final void updateProfileViewButton() { -        if (mProfileView == null) { -            return; -        } - -        final DisplayResolveInfo dri = -                mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile(); -        if (dri != null && !shouldShowTabs()) { -            mProfileView.setVisibility(View.VISIBLE); -            View text = mProfileView.findViewById(com.android.internal.R.id.profile_button); -            if (!(text instanceof TextView)) { -                text = mProfileView.findViewById(com.android.internal.R.id.text1); -            } -            ((TextView) text).setText(dri.getDisplayLabel()); -        } else { -            mProfileView.setVisibility(View.GONE); -        } -    } -      protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { -        final ActionTitle title = mLogic.getResolvingHome() +        final ActionTitle title = mResolvingHome                  ? ActionTitle.HOME                  : ActionTitle.forAction(intent.getAction()); @@ -1261,12 +1172,6 @@ public class ResolverActivity extends FragmentActivity implements          }      } -    final void dismiss() { -        if (!isFinishing()) { -            finish(); -        } -    } -      @Override      protected final void onRestart() {          super.onRestart(); @@ -1297,17 +1202,6 @@ public class ResolverActivity extends FragmentActivity implements              }          }          mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); -        updateProfileViewButton(); -    } - -    @Override -    protected final void onStart() { -        super.onStart(); - -        this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); -        if (hasWorkProfile()) { -            mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this); -        }      }      @Override @@ -1319,6 +1213,15 @@ public class ResolverActivity extends FragmentActivity implements          }      } +    @Override +    protected final void onStart() { +        super.onStart(); +        this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); +        if (hasWorkProfile()) { +            mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this); +        } +    } +      private boolean hasManagedProfile() {          UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);          if (userManager == null) { @@ -1340,7 +1243,7 @@ public class ResolverActivity extends FragmentActivity implements      private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {          try { -            ApplicationInfo appInfo = getPackageManager().getApplicationInfo( +            ApplicationInfo appInfo = mPackageManager.getApplicationInfo(                      resolveInfo.activityInfo.packageName, 0 /* default flags */);              return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;          } catch (NameNotFoundException e) { @@ -1358,7 +1261,8 @@ public class ResolverActivity extends FragmentActivity implements          // In case of clonedProfile being active, we do not allow the 'Always' option in the          // disambiguation dialog of Personal Profile as the package manager cannot distinguish          // between cross-profile preferred activities. -        if (hasCloneProfile() && (mMultiProfilePagerAdapter.getCurrentPage() == PROFILE_PERSONAL)) { +        if (hasCloneProfile() +                && (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)) {              mAlwaysButton.setEnabled(false);              return;          } @@ -1384,16 +1288,14 @@ public class ResolverActivity extends FragmentActivity implements          if (ri != null) {              ActivityInfo activityInfo = ri.activityInfo; -            boolean hasRecordPermission = -                    mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO, +            boolean hasRecordPermission = mPackageManager +                    .checkPermission(android.Manifest.permission.RECORD_AUDIO,                              activityInfo.packageName)                              == PackageManager.PERMISSION_GRANTED;              if (!hasRecordPermission) {                  // OK, we know the record permission, is this a capture device -                boolean hasAudioCapture = -                        getIntent().getBooleanExtra( -                                ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false); +                boolean hasAudioCapture = mResolverRequest.isAudioCaptureDevice();                  enabled = !hasAudioCapture;              }          } @@ -1406,10 +1308,8 @@ public class ResolverActivity extends FragmentActivity implements          if (isAutolaunching()) {              return;          } -        if (mIsIntentPicker) { -            ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) -                    .setUseLayoutWithDefault(useLayoutWithDefault()); -        } +        mMultiProfilePagerAdapter.setUseLayoutWithDefault(useLayoutWithDefault()); +          if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {              mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);          } else { @@ -1458,39 +1358,6 @@ public class ResolverActivity extends FragmentActivity implements          }      } -    @VisibleForTesting -    protected void safelyStartActivityInternal( -            TargetInfo cti, UserHandle user, @Nullable Bundle options) { -        // If the target is suspended, the activity will not be successfully launched. -        // Do not unregister from package manager updates in this case -        if (!cti.isSuspended() && mRegistered) { -            if (mPersonalPackageMonitor != null) { -                mPersonalPackageMonitor.unregister(); -            } -            if (mWorkPackageMonitor != null) { -                mWorkPackageMonitor.unregister(); -            } -            mRegistered = false; -        } -        // If needed, show that intent is forwarded -        // from managed profile to owner or other way around. -        String profileSwitchMessage = mLogic.getProfileSwitchMessage(); -        if (profileSwitchMessage != null) { -            Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); -        } -        try { -            if (cti.startAsCaller(this, options, user.getIdentifier())) { -                onActivityStarted(cti); -                maybeLogCrossProfileTargetLaunch(cti, user); -            } -        } catch (RuntimeException e) { -            Slog.wtf(TAG, -                    "Unable to launch as uid " + requireAnnotatedUserHandles().userIdOfCallingApp -                    + " package " + getLaunchedFromPackage() + ", while running in " -                    + ActivityThread.currentProcessName(), e); -        } -    } -      final void showTargetDetails(ResolveInfo ri) {          Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)                  .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) @@ -1511,7 +1378,7 @@ public class ResolverActivity extends FragmentActivity implements          // We partially rebuild the inactive adapter to determine if we should auto launch          // isTabLoaded will be true here if the empty state screen is shown instead of the list.          // To date, we really only care about "partially rebuilding" tabs for work and/or personal. -        boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildTabs(shouldShowTabs()); +        boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildTabs(hasWorkProfile());          if (shouldUseMiniResolver()) {              configureMiniResolverContent(targetDataLoader); @@ -1541,11 +1408,6 @@ public class ResolverActivity extends FragmentActivity implements          mLayoutId = R.layout.miniresolver;          setContentView(mLayoutId); -        // TODO: try to dedupe and use the pager's `getActiveProfile()` instead of the activity -        // `getCurrentProfile()` (or align them if they're not currently equivalent). If they truly -        // need to be distinct here, then `getCurrentProfile()` should at *least* get a more -        // specific name -- but note that checking `getCurrentProfile()` here, then following -        // `getActiveProfile()` to find the "in/active adapter," is exactly the legacy behavior.          boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;          ResolverListAdapter sameProfileAdapter = @@ -1604,17 +1466,69 @@ public class ResolverActivity extends FragmentActivity implements                  && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);      } +    @VisibleForTesting +    protected void safelyStartActivityInternal( +            TargetInfo cti, UserHandle user, @Nullable Bundle options) { +        // If the target is suspended, the activity will not be successfully launched. +        // Do not unregister from package manager updates in this case +        if (!cti.isSuspended() && mRegistered) { +            if (mPersonalPackageMonitor != null) { +                mPersonalPackageMonitor.unregister(); +            } +            if (mWorkPackageMonitor != null) { +                mWorkPackageMonitor.unregister(); +            } +            mRegistered = false; +        } +        // If needed, show that intent is forwarded +        // from managed profile to owner or other way around. +        String profileSwitchMessage = +                mIntentForwarding.forwardMessageFor(mResolverRequest.getIntent()); +        if (profileSwitchMessage != null) { +            Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); +        } +        try { +            if (cti.startAsCaller(this, options, user.getIdentifier())) { +                maybeLogCrossProfileTargetLaunch(cti, user); +            } +        } catch (RuntimeException e) { +            Slog.wtf(TAG, +                    "Unable to launch as uid " + mActivityModel.getLaunchedFromUid() +                    + " package " + getLaunchedFromPackage() + ", while running in " +                    + ActivityThread.currentProcessName(), e); +        } +    } + +    /** +     * Finishing procedures to be performed after the list has been rebuilt. +     * @param rebuildCompleted +     * @return <code>true</code> if the activity is finishing and creation should halt. +     */ +    final boolean postRebuildListInternal(boolean rebuildCompleted) { +        int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); + +        // We only rebuild asynchronously when we have multiple elements to sort. In the case where +        // we're already done, we can check if we should auto-launch immediately. +        if (rebuildCompleted && maybeAutolaunchActivity()) { +            return true; +        } + +        setupViewVisibilities(); + +        if (hasWorkProfile()) { +            setupProfileTabs(); +        } + +        return false; +    } +      /**       * Mini resolver should be used when all of the following are true:       * 1. This is the intent picker (ResolverActivity). -     * 2. There are exactly two tabs, for the "personal" and "work" profiles. -     * 3. This profile only has web browser matches. -     * 4. The other profile has a single non-browser match. +     * 2. This profile only has web browser matches. +     * 3. The other profile has a single non-browser match.       */      private boolean shouldUseMiniResolver() { -        if (!mIsIntentPicker) { -            return false; -        }          if (!isTwoPagePersonalAndWorkConfiguration()) {              return false;          } @@ -1652,50 +1566,6 @@ public class ResolverActivity extends FragmentActivity implements          return true;      } -    /** -     * Finishing procedures to be performed after the list has been rebuilt. -     * @param rebuildCompleted -     * @return <code>true</code> if the activity is finishing and creation should halt. -     */ -    final boolean postRebuildListInternal(boolean rebuildCompleted) { -        int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); - -        // We only rebuild asynchronously when we have multiple elements to sort. In the case where -        // we're already done, we can check if we should auto-launch immediately. -        if (rebuildCompleted && maybeAutolaunchActivity()) { -            return true; -        } - -        setupViewVisibilities(); - -        if (shouldShowTabs()) { -            setupProfileTabs(); -        } - -        return false; -    } - -    private int isPermissionGranted(String permission, int uid) { -        return ActivityManager.checkComponentPermission(permission, uid, -                /* owningUid= */-1, /* exported= */ true); -    } - -    /** -     * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} -     */ -    private boolean maybeAutolaunchActivity() { -        int numberOfProfiles = mMultiProfilePagerAdapter.getItemCount(); -        if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) { -            return true; -        } else if (maybeAutolaunchIfCrossProfileSupported()) { -            // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the -            //  correct intent-picker UIs (e.g., mini-resolver) if it was launched without -            //  ACTION_SEND. -            return true; -        } -        return false; -    } -      private boolean maybeAutolaunchIfSingleTarget() {          int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();          if (count != 1) { @@ -1761,7 +1631,7 @@ public class ResolverActivity extends FragmentActivity implements          }          String packageName = activeProfileTarget.getResolvedComponentName().getPackageName(); -        if (!canAppInteractCrossProfiles(packageName)) { +        if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {              return false;          } @@ -1776,131 +1646,66 @@ public class ResolverActivity extends FragmentActivity implements          return true;      } +    private boolean isAutolaunching() { +        return !mRegistered && isFinishing(); +    } +      /** -     * Returns whether the package has the necessary permissions to interact across profiles on -     * behalf of a given user. -     * -     * <p>This means meeting the following condition: -     * <ul> -     *     <li>The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least -     *     one of the following conditions must be fulfilled</li> -     *     <li>{@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.</li> -     *     <li>{@code Manifest.permission.INTERACT_ACROSS_USERS} granted.</li> -     *     <li>{@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding -     *     AppOps {@code android:interact_across_profiles} is set to "allow".</li> -     * </ul> -     * +     * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}       */ -    private boolean canAppInteractCrossProfiles(String packageName) { -        ApplicationInfo applicationInfo; -        try { -            applicationInfo = getPackageManager().getApplicationInfo(packageName, 0); -        } catch (NameNotFoundException e) { -            Log.e(TAG, "Package " + packageName + " does not exist on current user."); -            return false; -        } -        if (!applicationInfo.crossProfile) { +    private boolean maybeAutolaunchActivity() { +        if (!isTwoPagePersonalAndWorkConfiguration()) {              return false;          } -        int packageUid = applicationInfo.uid; +        ResolverListAdapter activeListAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                        ? mMultiProfilePagerAdapter.getPersonalListAdapter() +                        : mMultiProfilePagerAdapter.getWorkListAdapter(); -        if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, -                packageUid) == PackageManager.PERMISSION_GRANTED) { -            return true; -        } -        if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS, packageUid) -                == PackageManager.PERMISSION_GRANTED) { -            return true; +        ResolverListAdapter inactiveListAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                        ? mMultiProfilePagerAdapter.getWorkListAdapter() +                        : mMultiProfilePagerAdapter.getPersonalListAdapter(); + +        if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) { +            return false;          } -        if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES, -                PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) { -            return true; + +        if ((activeListAdapter.getUnfilteredCount() != 1) +                || (inactiveListAdapter.getUnfilteredCount() != 1)) { +            return false;          } -        return false; -    } -    private boolean isAutolaunching() { -        return !mRegistered && isFinishing(); -    } +        TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false); +        TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false); +        if (!Objects.equals( +                activeProfileTarget.getResolvedComponentName(), +                inactiveProfileTarget.getResolvedComponentName())) { +            return false; +        } -    private void setupProfileTabs() { -        maybeHideDivider(); -        TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost); -        tabHost.setup(); -        ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); -        viewPager.setSaveEnabled(false); - -        Button personalButton = (Button) getLayoutInflater().inflate( -                R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false); -        personalButton.setText(mDevicePolicyResources.getPersonalTabLabel()); -        personalButton.setContentDescription( -                mDevicePolicyResources.getPersonalTabAccessibilityLabel()); - -        TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL) -                .setContent(com.android.internal.R.id.profile_pager) -                .setIndicator(personalButton); -        tabHost.addTab(tabSpec); - -        Button workButton = (Button) getLayoutInflater().inflate( -                R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false); -        workButton.setText(mDevicePolicyResources.getWorkTabLabel()); -        workButton.setContentDescription(mDevicePolicyResources.getWorkTabAccessibilityLabel()); - -        tabSpec = tabHost.newTabSpec(TAB_TAG_WORK) -                .setContent(com.android.internal.R.id.profile_pager) -                .setIndicator(workButton); -        tabHost.addTab(tabSpec); - -        TabWidget tabWidget = tabHost.getTabWidget(); -        tabWidget.setVisibility(View.VISIBLE); -        updateActiveTabStyle(tabHost); - -        tabHost.setOnTabChangedListener(tabId -> { -            updateActiveTabStyle(tabHost); -            if (TAB_TAG_PERSONAL.equals(tabId)) { -                viewPager.setCurrentItem(0); -            } else { -                viewPager.setCurrentItem(1); -            } -            setupViewVisibilities(); -            maybeLogProfileChange(); -            onProfileTabSelected(); -            DevicePolicyEventLogger -                    .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) -                    .setInt(viewPager.getCurrentItem()) -                    .setStrings(getMetricsCategory()) -                    .write(); -        }); +        if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) { +            return false; +        } -        viewPager.setVisibility(View.VISIBLE); -        tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage()); -        mMultiProfilePagerAdapter.setOnProfileSelectedListener( -                new MultiProfilePagerAdapter.OnProfileSelectedListener() { -                    @Override -                    public void onProfileSelected(int index) { -                        tabHost.setCurrentTab(index); -                        resetButtonBar(); -                        resetCheckedItem(); -                    } +        String packageName = activeProfileTarget.getResolvedComponentName().getPackageName(); +        if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) { +            return false; +        } -                    @Override -                    public void onProfilePageStateChanged(int state) { -                        onHorizontalSwipeStateChanged(state); -                    } -                }); -        mOnSwitchOnWorkSelectedListener = () -> { -            final View workTab = tabHost.getTabWidget().getChildAt(1); -            workTab.setFocusable(true); -            workTab.setFocusableInTouchMode(true); -            workTab.requestFocus(); -        }; +        DevicePolicyEventLogger +                .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET) +                .setBoolean(activeListAdapter.getUserHandle() +                        .equals(requireAnnotatedUserHandles().personalProfileUserHandle)) +                .setStrings(getMetricsCategory()) +                .write(); +        safelyStartActivity(activeProfileTarget); +        finish(); +        return true;      }      private void maybeHideDivider() { -        if (!mIsIntentPicker) { -            return; -        }          final View divider = findViewById(com.android.internal.R.id.divider);          if (divider == null) {              return; @@ -1909,29 +1714,11 @@ public class ResolverActivity extends FragmentActivity implements      }      private void resetCheckedItem() { -        if (!mIsIntentPicker) { -            return; -        }          mLastSelected = ListView.INVALID_POSITION;          ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)                  .clearCheckedItemsInInactiveProfiles();      } -    private static int getAttrColor(Context context, int attr) { -        TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); -        int colorAccent = ta.getColor(0, 0); -        ta.recycle(); -        return colorAccent; -    } - -    private void updateActiveTabStyle(TabHost tabHost) { -        int currentTab = tabHost.getCurrentTab(); -        TextView selected = (TextView) tabHost.getTabWidget().getChildAt(currentTab); -        TextView unselected = (TextView) tabHost.getTabWidget().getChildAt(1 - currentTab); -        selected.setSelected(true); -        unselected.setSelected(false); -    } -      private void setupViewVisibilities() {          ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();          if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) { @@ -1957,10 +1744,7 @@ public class ResolverActivity extends FragmentActivity implements      private void setupAdapterListView(ListView listView, ItemClickListener listener) {          listView.setOnItemClickListener(listener);          listView.setOnItemLongClickListener(listener); - -        if (mLogic.getSupportsAlwaysUseOption()) { -            listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); -        } +        listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);      }      /** @@ -1971,7 +1755,7 @@ public class ResolverActivity extends FragmentActivity implements                  && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {              return;          } -        if (!shouldShowTabs() +        if (!hasWorkProfile()                  && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {              final TextView titleView = findViewById(com.android.internal.R.id.title);              if (titleView != null) { @@ -1979,10 +1763,9 @@ public class ResolverActivity extends FragmentActivity implements              }          } - -        CharSequence title = mLogic.getTitle() != null -                ? mLogic.getTitle() -                : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId()); +        CharSequence title = mResolverRequest.getTitle() != null +                ? mResolverRequest.getTitle() +                : getTitleForAction(mResolverRequest.getIntent(), 0);          if (!TextUtils.isEmpty(title)) {              final TextView titleView = findViewById(com.android.internal.R.id.title); @@ -2027,19 +1810,9 @@ public class ResolverActivity extends FragmentActivity implements      public final boolean useLayoutWithDefault() {          // We only use the default app layout when the profile of the active user has a          // filtered item. We always show the same default app even in the inactive user profile. -        boolean adapterForCurrentUserHasFilteredItem = -                mMultiProfilePagerAdapter.getListAdapterForUserHandle( -                        requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch -                ).hasFilteredItem(); -        return mLogic.getSupportsAlwaysUseOption() && adapterForCurrentUserHasFilteredItem; -    } - -    /** -     * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets -     * called and we are launched in a new task. -     */ -    protected final void setRetainInOnStop(boolean retainInOnStop) { -        mRetainInOnStop = retainInOnStop; +        return mMultiProfilePagerAdapter.getListAdapterForUserHandle( +                requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch +        ).hasFilteredItem();      }      final class ItemClickListener implements AdapterView.OnItemClickListener, @@ -2096,11 +1869,37 @@ public class ResolverActivity extends FragmentActivity implements      } -    /** Determine whether a given match result is considered "specific" in our application. */ -    public static final boolean isSpecificUriMatch(int match) { -        match = (match & IntentFilter.MATCH_CATEGORY_MASK); -        return match >= IntentFilter.MATCH_CATEGORY_HOST -                && match <= IntentFilter.MATCH_CATEGORY_PATH; +    private void setupProfileTabs() { +        maybeHideDivider(); + +        TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost); +        ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); + +        mMultiProfilePagerAdapter.setupProfileTabs( +                getLayoutInflater(), +                tabHost, +                viewPager, +                R.layout.resolver_profile_tab_button, +                com.android.internal.R.id.profile_pager, +                () -> onProfileTabSelected(viewPager.getCurrentItem()), +                new OnProfileSelectedListener() { +                    @Override +                    public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { +                        resetButtonBar(); +                        resetCheckedItem(); +                    } + +                    @Override +                    public void onProfilePageStateChanged(int state) {} +                }); +        mOnSwitchOnWorkSelectedListener = () -> { +            final View workTab = +                    tabHost.getTabWidget().getChildAt( +                            mMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK)); +            workTab.setFocusable(true); +            workTab.setFocusableInTouchMode(true); +            workTab.requestFocus(); +        };      }      static final class PickTargetOptionRequest extends PickOptionRequest { @@ -2173,7 +1972,7 @@ public class ResolverActivity extends FragmentActivity implements      private CharSequence getOrLoadDisplayLabel(TargetInfo info) {          if (info.isDisplayResolveInfo()) { -            mLogic.getTargetDataLoader().getOrLoadLabel((DisplayResolveInfo) info); +            mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info);          }          CharSequence displayLabel = info.getDisplayLabel();          return displayLabel == null ? "" : displayLabel; diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt index 0e2b25ec..7eb63ab3 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt @@ -1,81 +1,18 @@  package com.android.intentresolver.v2 -import android.content.Intent  import androidx.activity.ComponentActivity  import androidx.annotation.OpenForTesting -import com.android.intentresolver.R -import com.android.intentresolver.icons.DefaultTargetDataLoader -import com.android.intentresolver.icons.TargetDataLoader -import com.android.intentresolver.v2.util.mutableLazy  /** Activity logic for [ResolverActivity]. */  @OpenForTesting  open class ResolverActivityLogic(      tag: String, -    activityProvider: () -> ComponentActivity, +    activity: ComponentActivity,      onWorkProfileStatusUpdated: () -> Unit,  ) :      ActivityLogic,      CommonActivityLogic by CommonActivityLogicImpl(          tag, -        activityProvider, +        activity,          onWorkProfileStatusUpdated, -    ) { - -    override val targetIntent: Intent by lazy { -        val intent = Intent(activity.intent) -        intent.setComponent(null) -        // The resolver activity is set to be hidden from recent tasks. -        // we don't want this attribute to be propagated to the next activity -        // being launched. Note that if the original Intent also had this -        // flag set, we are now losing it. That should be a very rare case -        // and we can live with this. -        intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv()) - -        // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate -        // side, which means we want to open the target app on the same side as ResolverActivity. -        if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT != 0) { -            intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT.inv()) -        } -        intent -    } - -    override val resolvingHome: Boolean by lazy { -        targetIntent.action == Intent.ACTION_MAIN && -            targetIntent.categories.singleOrNull() == Intent.CATEGORY_HOME -    } - -    override val title: CharSequence? = null - -    override val defaultTitleResId: Int = 0 - -    override val initialIntents: List<Intent>? = null - -    override val supportsAlwaysUseOption: Boolean = true - -    override val targetDataLoader: TargetDataLoader by lazy { -        DefaultTargetDataLoader( -            activity, -            activity.lifecycle, -            activity.intent.getBooleanExtra( -                ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, -                /* defaultValue = */ false, -            ), -        ) -    } - -    override val themeResId: Int = R.style.Theme_DeviceDefault_Resolver - -    private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) } -    override val profileSwitchMessage: String? by _profileSwitchMessage - -    override val payloadIntents: List<Intent> by lazy { listOf(targetIntent) } - -    override fun preInitialization() { -        // Do nothing -    } - -    override fun clearProfileSwitchMessage() { -        _profileSwitchMessage.setLazy(null) -    } -} +    ) diff --git a/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt b/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt new file mode 100644 index 00000000..15c5018a --- /dev/null +++ b/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.annotation + +/** + * Apply to code which exists specifically to easy integration with existing Java and Java APIs. + * + * The goal is to prevent usage from Kotlin when a more idiomatic alternative is available. + */ +@RequiresOptIn("This is a a property, function or class specifically supporting Java " + +        "interoperability. Usage from Kotlin should be limited to interactions with Java.") +annotation class JavaInterop diff --git a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt index 7debdf07..5719ff08 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt @@ -16,6 +16,8 @@  package com.android.intentresolver.v2.data.repository  import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL +import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK  import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB  import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY  import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED @@ -28,41 +30,71 @@ import javax.inject.Inject  import javax.inject.Singleton  @Singleton -class DevicePolicyResources @Inject constructor( +class DevicePolicyResources +@Inject +constructor(      @ApplicationOwned private val resources: Resources,      devicePolicyManager: DevicePolicyManager  ) {      private val policyResources = devicePolicyManager.resources      val personalTabLabel by lazy { -        requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB) { -            resources.getString(R.string.resolver_personal_tab) -        }) +        requireNotNull( +            policyResources.getString(RESOLVER_PERSONAL_TAB) { +                resources.getString(R.string.resolver_personal_tab) +            } +        )      }      val workTabLabel by lazy { -        requireNotNull(policyResources.getString(RESOLVER_WORK_TAB) { -            resources.getString(R.string.resolver_work_tab) -        }) +        requireNotNull( +            policyResources.getString(RESOLVER_WORK_TAB) { +                resources.getString(R.string.resolver_work_tab) +            } +        )      }      val personalTabAccessibilityLabel by lazy { -        requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB_ACCESSIBILITY) { -            resources.getString(R.string.resolver_personal_tab_accessibility) -        }) +        requireNotNull( +            policyResources.getString(RESOLVER_PERSONAL_TAB_ACCESSIBILITY) { +                resources.getString(R.string.resolver_personal_tab_accessibility) +            } +        )      }      val workTabAccessibilityLabel by lazy { -        requireNotNull(policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) { -            resources.getString(R.string.resolver_work_tab_accessibility) -        }) +        requireNotNull( +            policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) { +                resources.getString(R.string.resolver_work_tab_accessibility) +            } +        ) +    } + +    val forwardToPersonalMessage: String? = +        devicePolicyManager.resources.getString(FORWARD_INTENT_TO_PERSONAL) { +            resources.getString(R.string.forward_intent_to_owner) +        } + +    val forwardToWorkMessage by lazy { +        requireNotNull( +            policyResources.getString(FORWARD_INTENT_TO_WORK) { +                resources.getString(R.string.forward_intent_to_work) +            } +        )      }      fun getWorkProfileNotSupportedMessage(launcherName: String): String { -        return requireNotNull(policyResources.getString(RESOLVER_WORK_PROFILE_NOT_SUPPORTED, { -            resources.getString( -                R.string.activity_resolver_work_profiles_support, -                launcherName) -        }, launcherName)) +        return requireNotNull( +            policyResources.getString( +                RESOLVER_WORK_PROFILE_NOT_SUPPORTED, +                { +                    resources.getString( +                        R.string.activity_resolver_work_profiles_support, +                        launcherName +                    ) +                }, +                launcherName +            ) +        )      } -}
\ No newline at end of file +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt index fc82efee..a0b2d1ef 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt @@ -1,8 +1,8 @@  package com.android.intentresolver.v2.data.repository  import android.content.pm.UserInfo -import com.android.intentresolver.v2.data.model.User -import com.android.intentresolver.v2.data.model.User.Role +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role  /** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */  fun UserInfo.getSupportedUserRole(): Role? = diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt index dc809b46..b57609e5 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt @@ -20,8 +20,8 @@ import com.android.intentresolver.inject.Background  import com.android.intentresolver.inject.Main  import com.android.intentresolver.inject.ProfileParent  import com.android.intentresolver.v2.data.broadcastFlow -import com.android.intentresolver.v2.data.model.User  import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent +import com.android.intentresolver.v2.shared.model.User  import dagger.hilt.android.qualifiers.ApplicationContext  import javax.inject.Inject  import kotlinx.coroutines.CoroutineDispatcher @@ -39,17 +39,17 @@ import kotlinx.coroutines.withContext  interface UserRepository {      /** -     * A [Flow] user profile groups. Each map contains the context user along with all members of +     * A [Flow] user profile groups. Each list contains the context user along with all members of       * the profile group. This includes the (Full) parent user, if the context user is a profile.       */ -    val users: Flow<Map<UserHandle, User>> +    val users: Flow<List<User>>      /**       * A [Flow] of availability. Only profile users may become unavailable.       *       * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].       */ -    fun isAvailable(user: User): Flow<Boolean> +    val availability: Flow<Map<User, Boolean>>      /**       * Request that availability be updated to the requested state. This currently includes toggling @@ -70,7 +70,7 @@ private const val TAG = "UserRepository"  private data class UserWithState(val user: User, val available: Boolean) -private typealias UserStateMap = Map<UserHandle, UserWithState> +private typealias UserStates = List<UserWithState>  /** Tracks and publishes state for the parent user and associated profiles. */  class UserRepositoryImpl @@ -110,15 +110,16 @@ constructor(          override val cause: Throwable? = null      ) : RuntimeException("$message: event=$event", cause) -    private val usersWithState: Flow<UserStateMap> = +    private val sharingScope = CoroutineScope(scope.coroutineContext + backgroundDispatcher) +    private val usersWithState: Flow<UserStates> =          userEvents              .onStart { emit(UserEvent(INITIALIZE, profileParent)) } -            .onEach { Log.i("UserDataSource", "userEvent: $it") } -            .runningFold<UserEvent, UserStateMap>(emptyMap()) { users, event -> +            .onEach { Log.i(TAG, "userEvent: $it") } +            .runningFold<UserEvent, UserStates>(emptyList()) { users, event ->                  try {                      // Handle an action by performing some operation, then returning a new map                      when (event.action) { -                        INITIALIZE -> createNewUserStateMap(profileParent) +                        INITIALIZE -> createNewUserStates(profileParent)                          ACTION_PROFILE_ADDED -> handleProfileAdded(event, users)                          ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users)                          ACTION_MANAGED_PROFILE_UNAVAILABLE, @@ -133,77 +134,67 @@ constructor(                  } catch (e: UserStateException) {                      Log.e(TAG, "An error occurred handling an event: ${e.event}", e)                      Log.e(TAG, "Attempting to recover...") -                    createNewUserStateMap(profileParent) +                    createNewUserStates(profileParent)                  }              } -            .onEach { Log.i("UserDataSource", "userStateMap: $it") } -            .stateIn(scope, SharingStarted.Eagerly, emptyMap()) +            .distinctUntilChanged() +            .onEach { Log.i(TAG, "userStateList: $it") } +            .stateIn(sharingScope, SharingStarted.Eagerly, emptyList())              .filterNot { it.isEmpty() } -    override val users: Flow<Map<UserHandle, User>> = -        usersWithState.map { map -> map.mapValues { it.value.user } }.distinctUntilChanged() +    override val users: Flow<List<User>> = +        usersWithState.map { userStateMap -> userStateMap.map { it.user } }.distinctUntilChanged() -    private val availability: Flow<Map<UserHandle, Boolean>> = -        usersWithState.map { map -> map.mapValues { it.value.available } }.distinctUntilChanged() - -    override fun isAvailable(user: User): Flow<Boolean> { -        return isAvailable(user.handle) -    } - -    @VisibleForTesting -    fun isAvailable(handle: UserHandle): Flow<Boolean> { -        return availability.map { it[handle] ?: false } -    } +    override val availability: Flow<Map<User, Boolean>> = +        usersWithState +            .map { list -> list.associate { it.user to it.available } } +            .distinctUntilChanged()      override suspend fun requestState(user: User, available: Boolean) {          require(user.type == User.Type.PROFILE) { "Only profile users are supported" } -        return requestState(user.handle, available) -    } - -    @VisibleForTesting -    suspend fun requestState(user: UserHandle, available: Boolean) {          return withContext(backgroundDispatcher) {              Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user") -            userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user) +            userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user.handle)          }      } -    private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap { +    private fun List<UserWithState>.update(handle: UserHandle, user: UserWithState) = +        filter { it.user.id != handle.identifier } + user + +    private fun handleAvailability(event: UserEvent, current: UserStates): UserStates {          val userEntry = -            current[event.user] +            current.firstOrNull { it.user.id == event.user.identifier }                  ?: throw UserStateException("User was not present in the map", event) -        return current + (event.user to userEntry.copy(available = !event.quietMode)) +        return current.update(event.user, userEntry.copy(available = !event.quietMode))      } -    private fun handleProfileRemoved(event: UserEvent, current: UserStateMap): UserStateMap { -        if (!current.containsKey(event.user)) { +    private fun handleProfileRemoved(event: UserEvent, current: UserStates): UserStates { +        if (!current.any { it.user.id == event.user.identifier }) {              throw UserStateException("User was not present in the map", event)          } -        return current.filterKeys { it != event.user } +        return current.filter { it.user.id != event.user.identifier }      } -    private suspend fun handleProfileAdded(event: UserEvent, current: UserStateMap): UserStateMap { +    private suspend fun handleProfileAdded(event: UserEvent, current: UserStates): UserStates {          val user =              try {                  requireNotNull(readUser(event.user))              } catch (e: Exception) {                  throw UserStateException("Failed to read user from UserManager", event, e)              } -        return current + (event.user to UserWithState(user, !event.quietMode)) +        return current + UserWithState(user, !event.quietMode)      } -    private suspend fun createNewUserStateMap(user: UserHandle): UserStateMap { +    private suspend fun createNewUserStates(user: UserHandle): UserStates {          val profiles = readProfileGroup(user) -        return profiles -            .mapNotNull { userInfo -> -                userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) } -            } -            .associateBy { it.user.handle } +        return profiles.mapNotNull { userInfo -> +            userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) } +        }      } -    private suspend fun readProfileGroup(handle: UserHandle): List<UserInfo> { +    private suspend fun readProfileGroup(member: UserHandle): List<UserInfo> {          return withContext(backgroundDispatcher) { -                @Suppress("DEPRECATION") userManager.getEnabledProfiles(handle.identifier) +                @Suppress("DEPRECATION") userManager.getEnabledProfiles(member.identifier)              }              .toList()      } diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt index 94f985e7..a84342f4 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt @@ -25,8 +25,11 @@ interface UserRepositoryModule {          @Provides          @Singleton          @ProfileParent -        fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle { -            return userManager.getProfileParent(user) ?: user +        fun profileParent( +            @ApplicationContext context: Context, +            userManager: UserManager +        ): UserHandle { +            return userManager.getProfileParent(context.user) ?: context.user          }      } diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt index 7ee78d91..3553744a 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt @@ -2,7 +2,7 @@ package com.android.intentresolver.v2.data.repository  import android.content.Context  import androidx.core.content.getSystemService -import com.android.intentresolver.v2.data.model.User +import com.android.intentresolver.v2.shared.model.User  /**   * Provides cached instances of a [system service][Context.getSystemService] created with diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt new file mode 100644 index 00000000..72b604c2 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.domain.interactor + +import android.os.UserHandle +import com.android.intentresolver.inject.ApplicationUser +import com.android.intentresolver.v2.data.repository.UserRepository +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.shared.model.Profile.Type +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +/** The high level User interface. */ +class UserInteractor +@Inject +constructor( +    private val userRepository: UserRepository, +    /** The specific [User] of the application which started this one. */ +    @ApplicationUser val launchedAs: UserHandle, +) { +    /** The profile group associated with the launching app user. */ +    val profiles: Flow<List<Profile>> = +        userRepository.users.map { users -> +            users.mapNotNull { user -> +                when (user.role) { +                    // PERSONAL includes CLONE +                    Role.PERSONAL -> { +                        Profile(Type.PERSONAL, user, users.firstOrNull { it.role == Role.CLONE }) +                    } +                    Role.CLONE -> { +                        /* ignore, included above */ +                        null +                    } +                    // others map 1:1 +                    else -> Profile(profileFromRole(user.role), user) +                } +            } +        } + +    /** The [Profile] of the application which started this one. */ +    val launchedAsProfile: Flow<Profile> = +        profiles.map { profiles -> +            // The launching user profile is the one with a primary id or clone id +            // matching the application user id. By definition there must always be exactly +            // one matching profile for the current user. +            profiles.single { +                it.primary.id == launchedAs.identifier || it.clone?.id == launchedAs.identifier +            } +        } +    /** +     * Provides a flow to report on the availability of profile. An unavailable profile may be +     * hidden or appear disabled within the app. +     */ +    val availability: Flow<Map<Profile, Boolean>> = +        combine(profiles, userRepository.availability) { profiles, availability -> +            profiles.associateWith { +                availability.getOrDefault(it.primary, false) +            } +        } + +    /** +     * Request the profile state be updated. In the case of enabling, the operation could take +     * significant time and/or require user input. +     */ +    suspend fun updateState(profile: Profile, available: Boolean) { +        userRepository.requestState(profile.primary, available) +    } + +    private fun profileFromRole(role: Role): Type = +        when (role) { +            Role.PERSONAL -> Type.PERSONAL +            Role.CLONE -> Type.PERSONAL /* CLONE maps to PERSONAL */ +            Role.PRIVATE -> Type.PRIVATE +            Role.WORK -> Type.WORK +        } +} diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java index e9d1bb34..dfc46697 100644 --- a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -29,11 +29,11 @@ import android.stats.devicepolicy.nano.DevicePolicyEnums;  import androidx.annotation.NonNull;  import androidx.annotation.Nullable; +import com.android.intentresolver.R;  import com.android.intentresolver.ResolvedComponentInfo;  import com.android.intentresolver.ResolverListAdapter;  import com.android.intentresolver.emptystate.EmptyState;  import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.internal.R;  import java.util.List; diff --git a/java/src/com/android/intentresolver/v2/emptystate/ResolverWorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/ResolverWorkProfilePausedEmptyStateProvider.java new file mode 100644 index 00000000..eaed35a7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/ResolverWorkProfilePausedEmptyStateProvider.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.emptystate; + +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; + +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; +import android.stats.devicepolicy.nano.DevicePolicyEnums; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.R; +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.WorkProfileAvailabilityManager; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; + +/** + * ResolverActivity empty state provider that returns empty state which is shown when + * work profile is paused and we need to show a button to enable it. + */ +public class ResolverWorkProfilePausedEmptyStateProvider implements EmptyStateProvider { + +    private final UserHandle mWorkProfileUserHandle; +    private final WorkProfileAvailabilityManager mWorkProfileAvailability; +    private final String mMetricsCategory; +    private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; +    private final Context mContext; + +    public ResolverWorkProfilePausedEmptyStateProvider(@NonNull Context context, +            @Nullable UserHandle workProfileUserHandle, +            @NonNull WorkProfileAvailabilityManager workProfileAvailability, +            @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener, +            @NonNull String metricsCategory) { +        mContext = context; +        mWorkProfileUserHandle = workProfileUserHandle; +        mWorkProfileAvailability = workProfileAvailability; +        mMetricsCategory = metricsCategory; +        mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener; +    } + +    @Nullable +    @Override +    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { +        if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle) +                || !mWorkProfileAvailability.isQuietModeEnabled() +                || resolverListAdapter.getCount() == 0) { +            return null; +        } + +        final String title = mContext.getSystemService(DevicePolicyManager.class) +                .getResources().getString(RESOLVER_WORK_PAUSED_TITLE, +                    () -> mContext.getString(R.string.resolver_turn_on_work_apps)); + +        return new WorkProfileOffEmptyState(title, (tab) -> { +            tab.showSpinner(); +            if (mOnSwitchOnWorkSelectedListener != null) { +                mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); +            } +            mWorkProfileAvailability.requestQuietModeEnabled(false); +        }, mMetricsCategory); +    } + +    public static class WorkProfileOffEmptyState implements EmptyState { + +        private final String mTitle; +        private final ClickListener mOnClick; +        private final String mMetricsCategory; + +        public WorkProfileOffEmptyState(String title, @NonNull ClickListener onClick, +                @NonNull String metricsCategory) { +            mTitle = title; +            mOnClick = onClick; +            mMetricsCategory = metricsCategory; +        } + +        @Nullable +        @Override +        public String getTitle() { +            return mTitle; +        } + +        @Nullable +        @Override +        public ClickListener getButtonClickListener() { +            return mOnClick; +        } + +        @Override +        public void onEmptyStateShown() { +            DevicePolicyEventLogger +                    .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED) +                    .setStrings(mMetricsCategory) +                    .write(); +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt b/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt new file mode 100644 index 00000000..6c36e6aa --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ext + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.bundleOf +import androidx.lifecycle.DEFAULT_ARGS_KEY +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.MutableCreationExtras + +/** + * Returns a new instance with additional [values] added to the existing default args Bundle (if + * present), otherwise adds a new entry with a copy of this bundle. + */ +fun CreationExtras.addDefaultArgs(vararg values: Pair<String, Parcelable>): CreationExtras { +    val defaultArgs: Bundle = get(DEFAULT_ARGS_KEY) ?: Bundle() +    defaultArgs.putAll(bundleOf(*values)) +    return MutableCreationExtras(this).apply { set(DEFAULT_ARGS_KEY, defaultArgs) } +} diff --git a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt new file mode 100644 index 00000000..8c2d7277 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.ext + +import android.content.Intent +import java.util.function.Predicate + +/** Applies an operation on this Intent if matches the given filter. */ +inline fun Intent.ifMatch( +    predicate: Predicate<Intent>, +    crossinline block: Intent.() -> Unit +): Intent { +    if (predicate.test(this)) { +        apply(block) +    } +    return this +} + +/** True if the Intent has one of the specified actions. */ +fun Intent.hasAction(vararg actions: String): Boolean = action in actions + +/** True if the Intent has a specific component target */ +fun Intent.hasComponent(): Boolean = (component != null) + +/** True if the Intent has a single matching category. */ +fun Intent.hasSingleCategory(category: String) = categories.singleOrNull() == category + +/** True if the Intent is a SEND or SEND_MULTIPLE action. */ +fun Intent.hasSendAction() = hasAction(Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE) + +/** True if the Intent resolves to the special Home (Launcher) component */ +fun Intent.isHomeIntent() = hasAction(Intent.ACTION_MAIN) && hasSingleCategory(Intent.CATEGORY_HOME) diff --git a/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt b/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt new file mode 100644 index 00000000..b0ec97f4 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ext + +import android.os.Parcel + +inline fun <reified T> Parcel.requireParcelable(): T { +    return requireNotNull(readParcelable<T>()) { "A non-value required from this parcel was null!" } +} + +inline fun <reified T> Parcel.readParcelable(): T? { +    return readParcelable(T::class.java.classLoader, T::class.java) +} diff --git a/java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt b/java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt new file mode 100644 index 00000000..9ca9d871 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.platform + +import android.content.pm.PackageManager +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class AppPredictionAvailable + +@Module +@InstallIn(SingletonComponent::class) +object AppPredictionModule { + +    /** +     * Eventually replaced with: Optional<AppPredictionRepository>, etc. +     */ +    @Provides +    @Singleton +    @AppPredictionAvailable +    fun isAppPredictionAvailable(packageManager: PackageManager): Boolean { +        return packageManager.appPredictionServicePackageName != null +    } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java new file mode 100644 index 00000000..c5b35273 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +/** + * Delegate to set up a given adapter and page view to be used together. + * + * @param <PageViewT>          (as in {@link MultiProfilePagerAdapter}). + * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}). + */ +public interface AdapterBinder<PageViewT, SinglePageAdapterT> { +    /** +     * The given {@code view} will be associated with the given {@code adapter}. Do any work +     * necessary to configure them compatibly, introduce them to each other, etc. +     */ +    void bind(PageViewT view, SinglePageAdapterT adapter); +} diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java index de0a9426..0ee9d141 100644 --- a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@   * limitations under the License.   */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles;  import android.content.Context;  import android.os.UserHandle; @@ -32,7 +32,6 @@ import com.android.intentresolver.R;  import com.android.intentresolver.emptystate.EmptyStateProvider;  import com.android.intentresolver.grid.ChooserGridAdapter;  import com.android.intentresolver.measurements.Tracer; -import com.android.internal.annotations.VisibleForTesting;  import com.google.common.collect.ImmutableList; @@ -42,7 +41,6 @@ import java.util.function.Supplier;  /**   * A {@link PagerAdapter} which describes the work and personal profile share sheet screens.   */ -@VisibleForTesting  public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<          RecyclerView, ChooserGridAdapter, ChooserListAdapter> {      private static final int SINGLE_CELL_SPAN_SIZE = 1; @@ -52,9 +50,10 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<      public ChooserMultiProfilePagerAdapter(              Context context, -            ChooserGridAdapter adapter, +            ImmutableList<TabConfig<ChooserGridAdapter>> tabs,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker, +            @ProfileType int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle,              int maxTargetsPerRow, @@ -62,31 +61,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<          this(                  context,                  new ChooserProfileAdapterBinder(maxTargetsPerRow), -                ImmutableList.of(adapter), -                emptyStateProvider, -                workProfileQuietModeChecker, -                /* defaultProfile= */ 0, -                workProfileUserHandle, -                cloneProfileUserHandle, -                new BottomPaddingOverrideSupplier(context), -                featureFlags); -    } - -    public ChooserMultiProfilePagerAdapter( -            Context context, -            ChooserGridAdapter personalAdapter, -            ChooserGridAdapter workAdapter, -            EmptyStateProvider emptyStateProvider, -            Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, -            UserHandle workProfileUserHandle, -            UserHandle cloneProfileUserHandle, -            int maxTargetsPerRow, -            FeatureFlags featureFlags) { -        this( -                context, -                new ChooserProfileAdapterBinder(maxTargetsPerRow), -                ImmutableList.of(personalAdapter, workAdapter), +                tabs,                  emptyStateProvider,                  workProfileQuietModeChecker,                  defaultProfile, @@ -99,10 +74,10 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<      private ChooserMultiProfilePagerAdapter(              Context context,              ChooserProfileAdapterBinder adapterBinder, -            ImmutableList<ChooserGridAdapter> gridAdapters, +            ImmutableList<TabConfig<ChooserGridAdapter>> tabs,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, +            @ProfileType int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle,              BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, @@ -110,7 +85,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<          super(                          gridAdapter -> gridAdapter.getListAdapter(),                  adapterBinder, -                gridAdapters, +                tabs,                  emptyStateProvider,                  workProfileQuietModeChecker,                  defaultProfile, @@ -137,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<       */      public void setIsCollapsed(boolean isCollapsed) {          for (int i = 0, size = getItemCount(); i < size; i++) { -            getAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); +            getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);          }      } @@ -172,7 +147,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<      /** Apply the specified {@code height} as the footer in each tab's adapter. */      public void setFooterHeightInEveryAdapter(int height) {          for (int i = 0; i < getItemCount(); ++i) { -            getAdapterForIndex(i).setFooterHeight(height); +            getPageAdapterForIndex(i).setFooterHeight(height);          }      } diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java index 2d9be816..43785db3 100644 --- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License. @@ -13,14 +13,18 @@   * See the License for the specific language governing permissions and   * limitations under the License.   */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles;  import android.annotation.IntDef;  import android.annotation.Nullable;  import android.os.Trace;  import android.os.UserHandle; +import android.view.LayoutInflater;  import android.view.View;  import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TabHost; +import android.widget.TextView;  import androidx.viewpager.widget.PagerAdapter;  import androidx.viewpager.widget.ViewPager; @@ -28,33 +32,21 @@ import androidx.viewpager.widget.ViewPager;  import com.android.intentresolver.ResolverListAdapter;  import com.android.intentresolver.emptystate.EmptyState;  import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper;  import com.android.internal.annotations.VisibleForTesting;  import com.google.common.collect.ImmutableList;  import java.util.HashSet; +import java.util.Objects;  import java.util.Optional;  import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer;  import java.util.function.Function;  import java.util.function.Supplier;  /**   * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet). - * <p> - * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose. - * <p> - * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive" - * <p> - * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident - * waiting to happen since clients seem to make assumptions about which adapter will be "active" in - * a particular context, and more explicit APIs would make sure those were valid. - * <p> - * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?) - * <p> - * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`. - * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base - * type and may be able to drop the type constraint.   *   * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter   * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in @@ -69,24 +61,11 @@ public class MultiProfilePagerAdapter<          SinglePageAdapterT,          ListAdapterT extends ResolverListAdapter> extends PagerAdapter { -    /** -     * Delegate to set up a given adapter and page view to be used together. -     * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}). -     * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}). -     */ -    public interface AdapterBinder<PageViewT, SinglePageAdapterT> { -        /** -         * The given {@code view} will be associated with the given {@code adapter}. Do any work -         * necessary to configure them compatibly, introduce them to each other, etc. -         */ -        void bind(PageViewT view, SinglePageAdapterT adapter); -    } -      public static final int PROFILE_PERSONAL = 0;      public static final int PROFILE_WORK = 1;      @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) -    public @interface Profile {} +    public @interface ProfileType {}      private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;      private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder; @@ -99,22 +78,21 @@ public class MultiProfilePagerAdapter<      private final UserHandle mCloneProfileUserHandle;      private final Supplier<Boolean> mWorkProfileQuietModeChecker;  // True when work is quiet. -    private Set<Integer> mLoadedPages; +    private final Set<Integer> mLoadedPages;      private int mCurrentPage;      private OnProfileSelectedListener mOnProfileSelectedListener;      protected MultiProfilePagerAdapter(              Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,              AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, -            ImmutableList<SinglePageAdapterT> adapters, +            ImmutableList<TabConfig<SinglePageAdapterT>> tabs,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, +            @ProfileType int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle,              Supplier<ViewGroup> pageViewInflater,              Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { -        mCurrentPage = defaultProfile;          mLoadedPages = new HashSet<>();          mWorkProfileUserHandle = workProfileUserHandle;          mCloneProfileUserHandle = cloneProfileUserHandle; @@ -127,21 +105,190 @@ public class MultiProfilePagerAdapter<          ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =                  new ImmutableList.Builder<>(); -        for (SinglePageAdapterT adapter : adapters) { -            items.add(createProfileDescriptor(adapter, containerBottomPaddingOverrideSupplier)); +        for (TabConfig<SinglePageAdapterT> tab : tabs) { +            // TODO: consider representing tabConfig in a different data structure that can ensure +            // uniqueness of their profile assignments (while still respecting the client's +            // requested tab order). +            items.add( +                    createProfileDescriptor( +                            tab.mProfile, +                            tab.mTabLabel, +                            tab.mTabAccessibilityLabel, +                            tab.mTabTag, +                            tab.mPageAdapter, +                            containerBottomPaddingOverrideSupplier));          }          mItems = items.build(); + +        mCurrentPage = +                hasPageForProfile(defaultProfile) ? getPageNumberForProfile(defaultProfile) : 0;      }      private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor( +            @ProfileType int profile, +            String tabLabel, +            String tabAccessibilityLabel, +            String tabTag,              SinglePageAdapterT adapter,              Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {          return new ProfileDescriptor<>( -                mPageViewInflater.get(), adapter, containerBottomPaddingOverrideSupplier); +                profile, +                tabLabel, +                tabAccessibilityLabel, +                tabTag, +                mPageViewInflater.get(), +                adapter, +                containerBottomPaddingOverrideSupplier); +    } + +    private boolean hasPageForIndex(int pageIndex) { +        return (pageIndex >= 0) && (pageIndex < getCount()); +    } + +    public final boolean hasPageForProfile(@ProfileType int profile) { +        return hasPageForIndex(getPageNumberForProfile(profile)); +    } + +    private @ProfileType int getProfileForPageNumber(int position) { +        if (hasPageForIndex(position)) { +            return mItems.get(position).mProfile; +        } +        return -1; +    } + +    public int getPageNumberForProfile(@ProfileType int profile) { +        for (int i = 0; i < mItems.size(); ++i) { +            if (profile == mItems.get(i).mProfile) { +                return i; +            } +        } +        return -1;      } -    public void setOnProfileSelectedListener(OnProfileSelectedListener listener) { -        mOnProfileSelectedListener = listener; +    private ListAdapterT getListAdapterForPageNumber(int pageNumber) { +        SinglePageAdapterT pageAdapter = getPageAdapterForIndex(pageNumber); +        if (pageAdapter == null) { +            return null; +        } +        return mListAdapterExtractor.apply(pageAdapter); +    } + +    private @ProfileType int getProfileForUserHandle(UserHandle userHandle) { +        if (userHandle.equals(getCloneUserHandle())) { +            // TODO: can we push this special case elsewhere -- e.g., when we check against each +            // list adapter's user handle in the loop below, could we instead ask the list adapter +            // whether it "represents" the queried user handle, and have the personal list adapter +            // return true because it knows it's also associated with the clone profile? Or if we +            // don't want to make modifications to the list adapter, maybe we could at least specify +            // it in our per-page configuration data that we use to build our tabs/pages, and then +            // maintain the relevant bookkeeping in our own ProfileDescriptor? +            return PROFILE_PERSONAL; +        } +        for (int i = 0; i < mItems.size(); ++i) { +            ListAdapterT listAdapter = getListAdapterForPageNumber(i); +            if (listAdapter.getUserHandle().equals(userHandle)) { +                return mItems.get(i).mProfile; +            } +        } +        return -1; +    } + +    private int getPageNumberForUserHandle(UserHandle userHandle) { +        return getPageNumberForProfile(getProfileForUserHandle(userHandle)); +    } + +    /** +     * Returns the {@link ListAdapterT} instance of the profile that represents +     * <code>userHandle</code>. If there is no such adapter for the specified +     * <code>userHandle</code>, returns {@code null}. +     * <p>For example, if there is a work profile on the device with user id 10, calling this method +     * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}. +     */ +    @Nullable +    public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { +        return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle)); +    } + +    @Nullable +    private ProfileDescriptor<PageViewT, SinglePageAdapterT> getDescriptorForUserHandle( +            UserHandle userHandle) { +        return getItem(getPageNumberForUserHandle(userHandle)); +    } + +    private int getPageNumberForTabTag(String tag) { +        for (int i = 0; i < mItems.size(); ++i) { +            if (Objects.equals(mItems.get(i).mTabTag, tag)) { +                return i; +            } +        } +        return -1; +    } + +    private void updateActiveTabStyle(TabHost tabHost) { +        int currentTab = tabHost.getCurrentTab(); + +        for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { +            // TODO: can we avoid this downcast by pushing our knowledge of the intended view type +            // somewhere else? +            TextView tabText = (TextView) tabHost.getTabWidget().getChildAt(pageNumber); +            tabText.setSelected(currentTab == pageNumber); +        } +    } + +    public void setupProfileTabs( +            LayoutInflater layoutInflater, +            TabHost tabHost, +            ViewPager viewPager, +            int tabButtonLayoutResId, +            int tabPageContentViewId, +            Runnable onTabChangeListener, +            OnProfileSelectedListener clientOnProfileSelectedListener) { +        tabHost.setup(); +        viewPager.setSaveEnabled(false); + +        for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { +            ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = mItems.get(pageNumber); +            Button profileButton = (Button) layoutInflater.inflate( +                    tabButtonLayoutResId, tabHost.getTabWidget(), false); +            profileButton.setText(descriptor.mTabLabel); +            profileButton.setContentDescription(descriptor.mTabAccessibilityLabel); + +            TabHost.TabSpec profileTabSpec = tabHost.newTabSpec(descriptor.mTabTag) +                    .setContent(tabPageContentViewId) +                    .setIndicator(profileButton); +            tabHost.addTab(profileTabSpec); +        } + +        tabHost.getTabWidget().setVisibility(View.VISIBLE); + +        updateActiveTabStyle(tabHost); + +        tabHost.setOnTabChangedListener(tabTag -> { +            updateActiveTabStyle(tabHost); + +            int pageNumber = getPageNumberForTabTag(tabTag); +            if (pageNumber >= 0) { +                viewPager.setCurrentItem(pageNumber); +            } +            onTabChangeListener.run(); +        }); + +        viewPager.setVisibility(View.VISIBLE); +        tabHost.setCurrentTab(getCurrentPage()); +        mOnProfileSelectedListener = +                new OnProfileSelectedListener() { +                    @Override +                    public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { +                        tabHost.setCurrentTab(pageNumber); +                        clientOnProfileSelectedListener.onProfilePageSelected( +                                profileId, pageNumber); +                    } + +                    @Override +                    public void onProfilePageStateChanged(int state) { +                        clientOnProfileSelectedListener.onProfilePageStateChanged(state); +                    } +                };      }      /** @@ -159,7 +306,8 @@ public class MultiProfilePagerAdapter<                      mLoadedPages.add(position);                  }                  if (mOnProfileSelectedListener != null) { -                    mOnProfileSelectedListener.onProfileSelected(position); +                    mOnProfileSelectedListener.onProfilePageSelected( +                            getProfileForPageNumber(position), position);                  }              } @@ -176,10 +324,7 @@ public class MultiProfilePagerAdapter<      }      public void clearInactiveProfileCache() { -        if (mLoadedPages.size() == 1) { -            return; -        } -        mLoadedPages.remove(1 - mCurrentPage); +        forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber));      }      @Override @@ -204,12 +349,8 @@ public class MultiProfilePagerAdapter<          return mCurrentPage;      } -    public final @Profile int getActiveProfile() { -        // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and -        // its mapped "page index." When we support more than two profiles, this won't be a "stable -        // mapping" -- some particular profile may not be represented by a "page," but the ones that -        // are will be assigned contiguous page numbers that skip over the holes. -        return getCurrentPage(); +    public final @ProfileType int getActiveProfile() { +        return getProfileForPageNumber(getCurrentPage());      }      @VisibleForTesting @@ -241,7 +382,11 @@ public class MultiProfilePagerAdapter<       * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>       * </ul>       */ +    @Nullable      private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) { +        if (!hasPageForIndex(pageIndex)) { +            return null; +        }          return mItems.get(pageIndex);      } @@ -263,7 +408,7 @@ public class MultiProfilePagerAdapter<      }      public final PageViewT getListViewForIndex(int index) { -        return getItem(index).mView; +        return getItem(index).getView();      }      /** @@ -273,8 +418,11 @@ public class MultiProfilePagerAdapter<       * depending on the adapter type.       */      @VisibleForTesting -    public final SinglePageAdapterT getAdapterForIndex(int index) { -        return getItem(index).mAdapter; +    public final SinglePageAdapterT getPageAdapterForIndex(int index) { +        if (!hasPageForIndex(index)) { +            return null; +        } +        return getItem(index).getAdapter();      }      /** @@ -282,26 +430,7 @@ public class MultiProfilePagerAdapter<       * by <code>pageIndex</code>.       */      public final void setupListAdapter(int pageIndex) { -        mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex)); -    } - -    /** -     * Returns the {@link ListAdapterT} instance of the profile that represents -     * <code>userHandle</code>. If there is no such adapter for the specified -     * <code>userHandle</code>, returns {@code null}. -     * <p>For example, if there is a work profile on the device with user id 10, calling this method -     * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}. -     */ -    @Nullable -    public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { -        if (getPersonalListAdapter().getUserHandle().equals(userHandle) -                || userHandle.equals(getCloneUserHandle())) { -            return getPersonalListAdapter(); -        } else if ((getWorkListAdapter() != null) -                && getWorkListAdapter().getUserHandle().equals(userHandle)) { -            return getWorkListAdapter(); -        } -        return null; +        mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex));      }      /** @@ -309,70 +438,35 @@ public class MultiProfilePagerAdapter<       * to the user.       * <p>For example, if the user is viewing the work tab in the share sheet, this method returns       * the work profile {@link ListAdapterT}. -     * @see #getInactiveListAdapter()       */      @VisibleForTesting      public final ListAdapterT getActiveListAdapter() { -        return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage())); -    } - -    /** -     * If this is a device with a work profile, returns the {@link ListAdapterT} instance -     * of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns -     * {@code null}. -     * <p>For example, if the user is viewing the work tab in the share sheet, this method returns -     * the personal profile {@link ListAdapterT}. -     * @see #getActiveListAdapter() -     */ -    @VisibleForTesting -    @Nullable -    public final ListAdapterT getInactiveListAdapter() { -        if (getCount() < 2) { -            return null; -        } -        return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage())); +        return getListAdapterForPageNumber(getCurrentPage());      }      public final ListAdapterT getPersonalListAdapter() { -        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL)); -    } - -    /** @return whether our tab data contains a page for the specified {@code profile} ID. */ -    public final boolean hasPageForProfile(@Profile int profile) { -        // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and -        // its mapped "page index." When we support more than two profiles, this won't be a "stable -        // mapping" -- some particular profile may not be represented by a "page," but the ones that -        // are will be assigned contiguous page numbers that skip over the holes. -        return hasAdapterForIndex(profile); +        return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL));      }      @Nullable      public final ListAdapterT getWorkListAdapter() { -        if (!hasAdapterForIndex(PROFILE_WORK)) { +        if (!hasPageForProfile(PROFILE_WORK)) {              return null;          } -        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); +        return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK));      }      public final SinglePageAdapterT getCurrentRootAdapter() { -        return getAdapterForIndex(getCurrentPage()); +        return getPageAdapterForIndex(getCurrentPage());      }      public final PageViewT getActiveAdapterView() {          return getListViewForIndex(getCurrentPage());      } -    @Nullable -    public final PageViewT getInactiveAdapterView() { -        if (getCount() < 2) { -            return null; -        } -        return getListViewForIndex(1 - getCurrentPage()); -    } -      private boolean anyAdapterHasItems() {          for (int i = 0; i < mItems.size(); ++i) { -            ListAdapterT listAdapter = mListAdapterExtractor.apply(getAdapterForIndex(i)); +            ListAdapterT listAdapter = getListAdapterForPageNumber(i);              if (listAdapter.getCount() > 0) {                  return true;              } @@ -381,13 +475,10 @@ public class MultiProfilePagerAdapter<      }      public void refreshPackagesInAllTabs() { -        // TODO: handle all inactive profiles; for now we can only have at most one. It's unclear if -        // this legacy logic really requires the active tab to be rebuilt first, or if we could just -        // iterate over the tabs in arbitrary order. +        // TODO: it's unclear if this legacy logic really requires the active tab to be rebuilt +        // first, or if we could just iterate over the tabs in arbitrary order.          getActiveListAdapter().handlePackagesChanged(); -        if (getCount() > 1) { -            getInactiveListAdapter().handlePackagesChanged(); -        } +        forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged());      }      /** @@ -445,9 +536,10 @@ public class MultiProfilePagerAdapter<          // autolaunch conditions).          boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded();          if (includePartialRebuildOfInactiveTabs) { -            boolean rebuildInactiveCompleted = -                    rebuildInactiveTab(false) || getInactiveListAdapter().isTabLoaded(); -            rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted; +            // Per legacy logic, avoid short-circuiting (TODO: why? possibly so that we *start* +            // loading the inactive tabs even if we're still waiting on the active tab to finish?). +            boolean completedRebuildingInactiveTabs = rebuildInactiveTabs(false); +            rebuildCompleted = rebuildCompleted && completedRebuildingInactiveTabs;          }          return rebuildCompleted;      } @@ -464,28 +556,43 @@ public class MultiProfilePagerAdapter<      }      /** -     * Rebuilds the tab that is not currently visible to the user, if such one exists. -     * <p>Returns {@code true} if rebuild has completed. +     * Rebuilds any tabs that are not currently visible to the user. +     * <p>Returns {@code true} if rebuild has completed in all inactive tabs.       */ -    private boolean rebuildInactiveTab(boolean doPostProcessing) { +    private boolean rebuildInactiveTabs(boolean doPostProcessing) {          Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); -        if (getItemCount() == 1) { -            Trace.endSection(); -            return false; -        } -        boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing); +        AtomicBoolean allRebuildsComplete = new AtomicBoolean(true); +        forEachInactivePage(pageNumber -> { +            // Evaluate the rebuild for every inactive page, even if we've already seen some adapter +            // return an "incomplete" status (i.e., even if `allRebuildsComplete` is already false) +            // and so we already know we'll end up returning false for the batch. +            // TODO: any particular reason the per-page legacy logic was set up in this order, or +            // could we possibly short-circuit the rebuild if the tab is already "loaded"? +            ListAdapterT inactiveAdapter = getListAdapterForPageNumber(pageNumber); +            boolean rebuildInactivePageCompleted = +                    rebuildTab(inactiveAdapter, doPostProcessing) || inactiveAdapter.isTabLoaded(); +            if (!rebuildInactivePageCompleted) { +                allRebuildsComplete.set(false); +            } +        });          Trace.endSection(); -        return result; +        return allRebuildsComplete.get();      } -    private int userHandleToPageIndex(UserHandle userHandle) { -        if (userHandle.equals(getPersonalListAdapter().getUserHandle())) { -            return PROFILE_PERSONAL; -        } else { -            return PROFILE_WORK; +    protected void forEachPage(Consumer<Integer> pageNumberHandler) { +        for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { +            pageNumberHandler.accept(pageNumber);          }      } +    protected void forEachInactivePage(Consumer<Integer> inactivePageNumberHandler) { +        forEachPage(pageNumber -> { +            if (pageNumber != getCurrentPage()) { +                inactivePageNumberHandler.accept(pageNumber); +            } +        }); +    } +      protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {          if (shouldSkipRebuild(activeListAdapter)) {              activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); @@ -499,10 +606,6 @@ public class MultiProfilePagerAdapter<          return emptyState != null && emptyState.shouldSkipDataRebuild();      } -    private boolean hasAdapterForIndex(int pageIndex) { -        return (pageIndex < getCount()); -    } -      /**       * The empty state screens are shown according to their priority:       * <ol> @@ -531,8 +634,8 @@ public class MultiProfilePagerAdapter<          if (emptyState.getButtonClickListener() != null) {              clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { -                ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem( -                        userHandleToPageIndex(listAdapter.getUserHandle())); +                ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = +                        getDescriptorForUserHandle(listAdapter.getUserHandle());                  descriptor.mEmptyStateUi.showSpinner();              });          } @@ -540,24 +643,12 @@ public class MultiProfilePagerAdapter<          showEmptyState(listAdapter, emptyState, clickListener);      } -    /** -     * Class to get user id of the current process -     */ -    public static class MyUserIdProvider { -        /** -         * @return user id of the current process -         */ -        public int getMyUserId() { -            return UserHandle.myUserId(); -        } -    } -      private void showEmptyState(              ListAdapterT activeListAdapter,              EmptyState emptyState,              View.OnClickListener buttonOnClick) { -        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem( -                userHandleToPageIndex(activeListAdapter.getUserHandle())); +        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = +                getDescriptorForUserHandle(activeListAdapter.getUserHandle());          descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);          activeListAdapter.markTabLoaded();      } @@ -571,8 +662,8 @@ public class MultiProfilePagerAdapter<      }      public void showListView(ListAdapterT activeListAdapter) { -        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem( -                userHandleToPageIndex(activeListAdapter.getUserHandle())); +        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = +                getDescriptorForUserHandle(activeListAdapter.getUserHandle());          descriptor.mEmptyStateUi.hide();      } @@ -581,11 +672,14 @@ public class MultiProfilePagerAdapter<       * application state.       */      public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() { -        if (getCount() < 2) { -            return false; -        } -        // TODO: check against *any* inactive adapter; for now we only have one. -        return shouldShowEmptyStateScreen(getInactiveListAdapter()); +        AtomicBoolean anyEmpty = new AtomicBoolean(false); +        // TODO: The "inactive" condition is legacy logic. Could we simplify and ask "any"? +        forEachInactivePage(pageNumber -> { +            if (shouldShowEmptyStateScreen(getListAdapterForPageNumber(pageNumber))) { +                anyEmpty.set(true); +            } +        }); +        return anyEmpty.get();      }      public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) { @@ -595,72 +689,4 @@ public class MultiProfilePagerAdapter<                      && mWorkProfileQuietModeChecker.get());      } -    // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" -    // should be the owner of all per-profile data (especially now that the API is generic)? -    private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> { -        final ViewGroup mRootView; -        final EmptyStateUiHelper mEmptyStateUi; - -        // TODO: post-refactoring, we may not need to retain these ivars directly (since they may -        // be encapsulated within the `EmptyStateUiHelper`?). -        private final ViewGroup mEmptyStateView; - -        private final SinglePageAdapterT mAdapter; -        private final PageViewT mView; - -        ProfileDescriptor( -                ViewGroup rootView, -                SinglePageAdapterT adapter, -                Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { -            mRootView = rootView; -            mAdapter = adapter; -            mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); -            mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); -            mEmptyStateUi = new EmptyStateUiHelper( -                    rootView, -                    com.android.internal.R.id.resolver_list, -                    containerBottomPaddingOverrideSupplier); -        } - -        protected ViewGroup getEmptyStateView() { -            return mEmptyStateView; -        } - -        private void setupContainerPadding() { -            mEmptyStateUi.setupContainerPadding(); -        } -    } - -    /** Listener interface for changes between the per-profile UI tabs. */ -    public interface OnProfileSelectedListener { -        /** -         * Callback for when the user changes the active tab from personal to work or vice versa. -         * <p>This callback is only called when the intent resolver or share sheet shows -         * the work and personal profiles. -         * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or -         * {@link #PROFILE_WORK} if the work profile was selected. -         */ -        void onProfileSelected(int profileIndex); - - -        /** -         * Callback for when the scroll state changes. Useful for discovering when the user begins -         * dragging, when the pager is automatically settling to the current page, or when it is -         * fully stopped/idle. -         * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} -         *              or {@link ViewPager#SCROLL_STATE_SETTLING} -         * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged -         */ -        void onProfilePageStateChanged(int state); -    } - -    /** -     * Listener for when the user switches on the work profile from the work tab. -     */ -    public interface OnSwitchOnWorkSelectedListener { -        /** -         * Callback for when the user switches on the work profile from the work tab. -         */ -        void onSwitchOnWorkSelected(); -    }  } diff --git a/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java new file mode 100644 index 00000000..7bdbec4c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import androidx.viewpager.widget.ViewPager; + +/** Listener interface for changes between the per-profile UI tabs. */ +public interface OnProfileSelectedListener { +    /** +     * Callback for when the user changes the active tab. +     * <p>This callback is only called when the intent resolver or share sheet shows +     * more than one profile. +     * +     * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL} +     *                  if the personal profile tab was selected or {@link #PROFILE_WORK} if the +     *                  work profile tab +     *                  was selected. +     */ +    void onProfilePageSelected(@MultiProfilePagerAdapter.ProfileType int profileId, int pageNumber); + + +    /** +     * Callback for when the scroll state changes. Useful for discovering when the user begins +     * dragging, when the pager is automatically settling to the current page, or when it is +     * fully stopped/idle. +     * +     * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} +     *              or {@link ViewPager#SCROLL_STATE_SETTLING} +     * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged +     */ +    void onProfilePageStateChanged(int state); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java new file mode 100644 index 00000000..3dbbd4d0 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +/** + * Listener for when the user switches on the work profile from the work tab. + */ +public interface OnSwitchOnWorkSelectedListener { +    /** +     * Callback for when the user switches on the work profile from the work tab. +     */ +    void onSwitchOnWorkSelected(); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java new file mode 100644 index 00000000..e2e9c19d --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import android.view.ViewGroup; + +import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper; + +import java.util.Optional; +import java.util.function.Supplier; + +// TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" +// should be the owner of all per-profile data (especially now that the API is generic)? +class ProfileDescriptor<PageViewT, SinglePageAdapterT> { +    final @MultiProfilePagerAdapter.ProfileType int mProfile; +    final String mTabLabel; +    final String mTabAccessibilityLabel; +    final String mTabTag; + +    final ViewGroup mRootView; +    final EmptyStateUiHelper mEmptyStateUi; + +    // TODO: post-refactoring, we may not need to retain these ivars directly (since they may +    // be encapsulated within the `EmptyStateUiHelper`?). +    private final ViewGroup mEmptyStateView; + +    private final SinglePageAdapterT mAdapter; + +    public SinglePageAdapterT getAdapter() { +        return mAdapter; +    } + +    public PageViewT getView() { +        return mView; +    } + +    private final PageViewT mView; + +    ProfileDescriptor( +            @MultiProfilePagerAdapter.ProfileType int forProfile, +            String tabLabel, +            String tabAccessibilityLabel, +            String tabTag, +            ViewGroup rootView, +            SinglePageAdapterT adapter, +            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { +        mProfile = forProfile; +        mTabLabel = tabLabel; +        mTabAccessibilityLabel = tabAccessibilityLabel; +        mTabTag = tabTag; +        mRootView = rootView; +        mAdapter = adapter; +        mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); +        mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); +        mEmptyStateUi = new EmptyStateUiHelper( +                rootView, +                com.android.internal.R.id.resolver_list, +                containerBottomPaddingOverrideSupplier); +    } + +    protected ViewGroup getEmptyStateView() { +        return mEmptyStateView; +    } + +    public void setupContainerPadding() { +        mEmptyStateUi.setupContainerPadding(); +    } +} diff --git a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java index d96fd15a..e44cf8da 100644 --- a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@   * limitations under the License.   */ -package com.android.intentresolver.v2; +package com.android.intentresolver.v2.profiles;  import android.content.Context;  import android.os.UserHandle; @@ -27,7 +27,6 @@ import androidx.viewpager.widget.PagerAdapter;  import com.android.intentresolver.R;  import com.android.intentresolver.ResolverListAdapter;  import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.internal.annotations.VisibleForTesting;  import com.google.common.collect.ImmutableList; @@ -37,40 +36,20 @@ import java.util.function.Supplier;  /**   * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.   */ -@VisibleForTesting  public class ResolverMultiProfilePagerAdapter extends          MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {      private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; -    public ResolverMultiProfilePagerAdapter( -            Context context, -            ResolverListAdapter adapter, -            EmptyStateProvider emptyStateProvider, -            Supplier<Boolean> workProfileQuietModeChecker, -            UserHandle workProfileUserHandle, -            UserHandle cloneProfileUserHandle) { -        this( -                context, -                ImmutableList.of(adapter), -                emptyStateProvider, -                workProfileQuietModeChecker, -                /* defaultProfile= */ 0, -                workProfileUserHandle, -                cloneProfileUserHandle, -                new BottomPaddingOverrideSupplier()); -    } -      public ResolverMultiProfilePagerAdapter(Context context, -                                            ResolverListAdapter personalAdapter, -                                            ResolverListAdapter workAdapter, +                                            ImmutableList<TabConfig<ResolverListAdapter>> tabs,                                              EmptyStateProvider emptyStateProvider,                                              Supplier<Boolean> workProfileQuietModeChecker, -                                            @Profile int defaultProfile, +                                            @ProfileType int defaultProfile,                                              UserHandle workProfileUserHandle,                                              UserHandle cloneProfileUserHandle) {          this(                  context, -                ImmutableList.of(personalAdapter, workAdapter), +                tabs,                  emptyStateProvider,                  workProfileQuietModeChecker,                  defaultProfile, @@ -81,17 +60,17 @@ public class ResolverMultiProfilePagerAdapter extends      private ResolverMultiProfilePagerAdapter(              Context context, -            ImmutableList<ResolverListAdapter> listAdapters, +            ImmutableList<TabConfig<ResolverListAdapter>> tabs,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, +            @ProfileType int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle,              BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {          super(                          listAdapter -> listAdapter,                          (listView, bindAdapter) -> listView.setAdapter(bindAdapter), -                listAdapters, +                tabs,                  emptyStateProvider,                  workProfileQuietModeChecker,                  defaultProfile, @@ -109,11 +88,13 @@ public class ResolverMultiProfilePagerAdapter extends      /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */      public void clearCheckedItemsInInactiveProfiles() { -        // TODO: apply to all inactive adapters; for now we just have the one. -        ListView inactiveListView = getInactiveAdapterView(); -        if (inactiveListView.getCheckedItemCount() > 0) { -            inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false); -        } +        // TODO: The "inactive" condition is legacy logic. Could we simplify and clear-all? +        forEachInactivePage(pageNumber -> { +            ListView inactiveListView = getListViewForIndex(pageNumber); +            if (inactiveListView.getCheckedItemCount() > 0) { +                inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false); +            } +        });      }      private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> { diff --git a/java/src/com/android/intentresolver/v2/profiles/TabConfig.java b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java new file mode 100644 index 00000000..994f8aff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +public class TabConfig<PageAdapterT> { +    final @MultiProfilePagerAdapter.ProfileType int mProfile; +    final String mTabLabel; +    final String mTabAccessibilityLabel; +    final String mTabTag; +    final PageAdapterT mPageAdapter; + +    public TabConfig( +            @MultiProfilePagerAdapter.ProfileType int profile, +            String tabLabel, +            String tabAccessibilityLabel, +            String tabTag, +            PageAdapterT pageAdapter) { +        mProfile = profile; +        mTabLabel = tabLabel; +        mTabAccessibilityLabel = tabAccessibilityLabel; +        mTabTag = tabTag; +        mPageAdapter = pageAdapter; +    } +} diff --git a/java/src/com/android/intentresolver/v2/shared/model/Profile.kt b/java/src/com/android/intentresolver/v2/shared/model/Profile.kt new file mode 100644 index 00000000..6e37174c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/shared/model/Profile.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.shared.model + +import com.android.intentresolver.v2.shared.model.Profile.Type + +/** + * Associates [users][User] into a [Type] instance. + * + * This is a simple abstraction which combines a primary [user][User] with an optional + * [cloned apps][User.Role.CLONE] user. This encapsulates the cloned app user id, while still being + * available where needed. + */ +data class Profile( +    val type: Type, +    val primary: User, +    /** +     * An optional [User] of which contains second instances of some applications installed for the +     * personal user. This value may only be supplied when creating the PERSONAL profile. +     */ +    val clone: User? = null +) { + +    init { +        clone?.apply { +            require(primary.role == User.Role.PERSONAL) { +                "clone is not supported for profile=${this@Profile.type} / primary=$primary" +            } +            require(role == User.Role.CLONE) { "clone is not a clone user ($this)" } +        } +    } + +    enum class Type { +        PERSONAL, +        WORK, +        PRIVATE +    } +} diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/shared/model/User.kt index 504b04c8..97db3280 100644 --- a/java/src/com/android/intentresolver/v2/data/model/User.kt +++ b/java/src/com/android/intentresolver/v2/shared/model/User.kt @@ -1,10 +1,25 @@ -package com.android.intentresolver.v2.data.model +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.shared.model  import android.annotation.UserIdInt  import android.os.UserHandle -import com.android.intentresolver.v2.data.model.User.Type -import com.android.intentresolver.v2.data.model.User.Type.FULL -import com.android.intentresolver.v2.data.model.User.Type.PROFILE +import com.android.intentresolver.v2.shared.model.User.Type.FULL +import com.android.intentresolver.v2.shared.model.User.Type.PROFILE  /**   * A User represents the owner of a distinct set of content. diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java index 271c6f38..a1e1c7fa 100644 --- a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java +++ b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java @@ -21,7 +21,6 @@ import android.provider.MediaStore;  import androidx.annotation.StringRes;  import com.android.intentresolver.R; -import com.android.intentresolver.v2.ResolverActivity;  /**   * Provides a set of related resources for different use cases. diff --git a/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt b/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt new file mode 100644 index 00000000..1cd72ba5 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ui + +import android.content.res.Resources +import com.android.intentresolver.inject.ApplicationOwned +import com.android.intentresolver.v2.data.repository.DevicePolicyResources +import com.android.intentresolver.v2.shared.model.Profile +import javax.inject.Inject +import com.android.intentresolver.R + +class ProfilePagerResources +@Inject +constructor( +    @ApplicationOwned private val resources: Resources, +    private val devicePolicyResources: DevicePolicyResources +) { +    private val privateTabLabel by lazy { resources.getString(R.string.resolver_private_tab) } + +    private val privateTabAccessibilityLabel by lazy { +        resources.getString(R.string.resolver_private_tab_accessibility) +    } + +    fun profileTabLabel(profile: Profile.Type): String { +        return when (profile) { +            Profile.Type.PERSONAL -> devicePolicyResources.personalTabLabel +            Profile.Type.WORK -> devicePolicyResources.workTabLabel +            Profile.Type.PRIVATE -> privateTabLabel +        } +    } + +    fun profileTabAccessibilityLabel(type: Profile.Type): String { +        return when (type) { +            Profile.Type.PERSONAL -> devicePolicyResources.personalTabAccessibilityLabel +            Profile.Type.WORK -> devicePolicyResources.workTabAccessibilityLabel +            Profile.Type.PRIVATE -> privateTabAccessibilityLabel +        } +    } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt b/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt new file mode 100644 index 00000000..2b01b5e7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ui + +import android.app.Activity +import android.app.compat.CompatChanges +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.service.chooser.ChooserResult +import android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY +import android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT +import android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT +import android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN +import android.service.chooser.ChooserResult.ResultType +import android.util.Log +import com.android.intentresolver.inject.Background +import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.inject.Main +import com.android.intentresolver.v2.ui.model.ShareAction +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ActivityContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private const val TAG = "ShareResultSender" + +/** Reports the result of a share to another process across binder, via an [IntentSender] */ +interface ShareResultSender { +    /** Reports user selection of an activity to launch from the provided choices. */ +    fun onComponentSelected(component: ComponentName, directShare: Boolean) + +    /** Reports user invocation of a built-in system action. See [ShareAction]. */ +    fun onActionSelected(action: ShareAction) +} + +@AssistedFactory +interface ShareResultSenderFactory { +    fun create(callerUid: Int, chosenComponentSender: IntentSender): ShareResultSenderImpl +} + +/** Dispatches Intents via IntentSender */ +fun interface IntentSenderDispatcher { +    fun dispatchIntent(intentSender: IntentSender, intent: Intent) +} + +class ShareResultSenderImpl( +    private val flags: ChooserServiceFlags, +    @Main private val scope: CoroutineScope, +    @Background val backgroundDispatcher: CoroutineDispatcher, +    private val callerUid: Int, +    private val resultSender: IntentSender, +    private val intentDispatcher: IntentSenderDispatcher +) : ShareResultSender { +    @AssistedInject +    constructor( +        @ActivityContext context: Context, +        flags: ChooserServiceFlags, +        @Main scope: CoroutineScope, +        @Background backgroundDispatcher: CoroutineDispatcher, +        @Assisted callerUid: Int, +        @Assisted chosenComponentSender: IntentSender, +    ) : this( +        flags, +        scope, +        backgroundDispatcher, +        callerUid, +        chosenComponentSender, +        IntentSenderDispatcher { sender, intent -> sender.dispatchIntent(context, intent) } +    ) + +    override fun onComponentSelected(component: ComponentName, directShare: Boolean) { +        Log.i(TAG, "onComponentSelected: $component directShare=$directShare") +        scope.launch { +            val intent = createChosenComponentIntent(component, directShare) +            intentDispatcher.dispatchIntent(resultSender, intent) +        } +    } + +    override fun onActionSelected(action: ShareAction) { +        Log.i(TAG, "onActionSelected: $action") +        scope.launch { +            if (flags.enableChooserResult() && chooserResultSupported(callerUid)) { +                @ResultType val chosenAction = shareActionToChooserResult(action) +                val intent: Intent = createSelectedActionIntent(chosenAction) +                intentDispatcher.dispatchIntent(resultSender, intent) +            } else { +                Log.i(TAG, "Not sending SelectedAction") +            } +        } +    } + +    private suspend fun createChosenComponentIntent( +        component: ComponentName, +        direct: Boolean, +    ): Intent { +        // Add extra with component name for backwards compatibility. +        val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component) + +        // Add ChooserResult value for Android V+ +        if (flags.enableChooserResult() && chooserResultSupported(callerUid)) { +            intent.putExtra( +                Intent.EXTRA_CHOOSER_RESULT, +                ChooserResult(CHOOSER_RESULT_SELECTED_COMPONENT, component, direct) +            ) +        } else { +            Log.i(TAG, "Not including ${Intent.EXTRA_CHOOSER_RESULT}") +        } +        return intent +    } + +    @ResultType +    private fun shareActionToChooserResult(action: ShareAction) = +        when (action) { +            ShareAction.SYSTEM_COPY -> CHOOSER_RESULT_COPY +            ShareAction.SYSTEM_EDIT -> CHOOSER_RESULT_EDIT +            ShareAction.APPLICATION_DEFINED -> CHOOSER_RESULT_UNKNOWN +        } + +    private fun createSelectedActionIntent(@ResultType result: Int): Intent { +        return Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult(result, null, false)) +    } + +    private suspend fun chooserResultSupported(uid: Int): Boolean { +        return withContext(backgroundDispatcher) { +            // background -> Binder call to system_server +            CompatChanges.isChangeEnabled(ChooserResult.SEND_CHOOSER_RESULT, uid) +        } +    } +} + +private fun IntentSender.dispatchIntent(context: Context, intent: Intent) { +    try { +        sendIntent( +            /* context = */ context, +            /* code = */ Activity.RESULT_OK, +            /* intent = */ intent, +            /* onFinished = */ null, +            /* handler = */ null +        ) +    } catch (e: IntentSender.SendIntentException) { +        Log.e(TAG, "Failed to send intent to IntentSender", e) +    } +} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt new file mode 100644 index 00000000..07b17435 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.ui.model + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import com.android.intentresolver.v2.ext.readParcelable +import com.android.intentresolver.v2.ext.requireParcelable +import java.util.Objects + +/** Contains Activity-scope information about the state when started. */ +data class ActivityModel( +    /** The [Intent] received by the app */ +    val intent: Intent, +    /** The identifier for the sending app and user */ +    val launchedFromUid: Int, +    /** The package of the sending app */ +    val launchedFromPackage: String, +    /** The referrer as supplied to the activity. */ +    val referrer: Uri? +) : Parcelable { +    constructor( +        source: Parcel +    ) : this( +        intent = source.requireParcelable(), +        launchedFromUid = source.readInt(), +        launchedFromPackage = requireNotNull(source.readString()), +        referrer = source.readParcelable() +    ) + +    /** A package name from referrer, if it is an android-app URI */ +    val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority + +    override fun describeContents() = 0 /* flags */ + +    override fun writeToParcel(dest: Parcel, flags: Int) { +        dest.writeParcelable(intent, flags) +        dest.writeInt(launchedFromUid) +        dest.writeString(launchedFromPackage) +        dest.writeParcelable(referrer, flags) +    } + +    companion object { +        const val ACTIVITY_MODEL_KEY = "com.android.intentresolver.ACTIVITY_MODEL" + +        @JvmField +        @Suppress("unused") +        val CREATOR = +            object : Parcelable.Creator<ActivityModel> { +                override fun newArray(size: Int) = arrayOfNulls<ActivityModel>(size) +                override fun createFromParcel(source: Parcel) = ActivityModel(source) +            } + +        @JvmStatic +        fun createFrom(activity: Activity): ActivityModel { +            return ActivityModel( +                activity.intent, +                activity.launchedFromUid, +                Objects.requireNonNull<String>(activity.launchedFromPackage), +                activity.referrer +            ) +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt new file mode 100644 index 00000000..4f3cf3cd --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.ui.model + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.Intent.EXTRA_REFERRER +import android.content.IntentFilter +import android.content.IntentSender +import android.net.Uri +import android.os.Bundle +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import androidx.annotation.StringRes +import com.android.intentresolver.ContentTypeHint +import com.android.intentresolver.v2.ext.hasAction + +const val ANDROID_APP_SCHEME = "android-app" + +/** All of the things that are consumed from an incoming share Intent (+Extras). */ +data class ChooserRequest( +    /** Required. Represents the content being sent. */ +    val targetIntent: Intent, + +    /** The action from [targetIntent] as retrieved with [Intent.getAction]. */ +    val targetAction: String?, + +    /** +     * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the +     * canonical "Share" actions. When handling other actions, this flag controls behavioral and +     * visual changes. +     */ +    val isSendActionTarget: Boolean, + +    /** The top-level content type as retrieved using [Intent.getType]. */ +    val targetType: String?, + +    /** The package name of the app which started the current activity instance. */ +    val launchedFromPackage: String, + +    /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */ +    val title: CharSequence? = null, + +    /** A String resource ID to load when [title] is null. */ +    @get:StringRes val defaultTitleResource: Int = 0, + +    /** +     * The referrer value as received by the caller. It may have been supplied via [EXTRA_REFERRER] +     * or synthesized from callerPackageName. This value is merged into outgoing intents. +     */ +    val referrer: Uri?, + +    /** +     * Choices to exclude from results. +     * +     * Any resolved intents with a component in this list will be omitted before presentation. +     */ +    val filteredComponentNames: List<ComponentName> = emptyList(), + +    /** +     * App provided shortcut share intents (aka "direct share targets") +     * +     * Normally share shortcuts are published and consumed using +     * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow +     * apps to directly inject the same information. +     * +     * Historical note: This option was initially integrated with other results from the +     * ChooserTargetService API (since deprecated and removed), hence the name and data format. +     * These are more correctly called "Share Shortcuts" now. +     */ +    val callerChooserTargets: List<ChooserTarget> = emptyList(), + +    /** +     * Actions the user may perform. These are presented as separate affordances from the main list +     * of choices. Selecting a choice is a terminal action which results in finishing. The item +     * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate. +     */ +    val chooserActions: List<ChooserAction> = emptyList(), + +    /** +     * An action to start an Activity which for user updating of shared content. Selection is a +     * terminal action, closing the current activity and launching the target of the action. +     */ +    val modifyShareAction: ChooserAction? = null, + +    /** +     * When false the host activity will be [finished][android.app.Activity.finish] when stopped. +     */ +    @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false, + +    /** +     * Intents which contain alternate representations of the content being shared. Any results from +     * resolving these _alternate_ intents are included with the results of the primary intent as +     * additional choices (e.g. share as image content vs. link to content). +     */ +    val additionalTargets: List<Intent> = emptyList(), + +    /** +     * Alternate [extras][Intent.getExtras] to substitute when launching a selected app. +     * +     * For a given app (by package name), the Bundle describes what parameters to substitute when +     * that app is selected. +     * +     * // TODO: Map<String, Bundle> +     */ +    val replacementExtras: Bundle? = null, + +    /** +     * App-supplied choices to be presented first in the list. +     * +     * Custom labels and icons may be supplied using +     * [LabeledIntent][android.content.pm.LabeledIntent]. +     * +     * Limit 2. +     */ +    val initialIntents: List<Intent> = emptyList(), + +    /** +     * Provides for callers to be notified when a component is selected. +     * +     * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the +     * [ComponentName] of the item. +     */ +    val chosenComponentSender: IntentSender? = null, + +    /** +     * Provides a mechanism for callers to post-process a target when a selection is made. +     * +     * The received intent will contain: +     * * **EXTRA_INTENT** The chosen target +     * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target +     * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a +     *   mechanism for the caller to return information. An updated intent to send must be included +     *   as [Intent.EXTRA_INTENT]. +     */ +    val refinementIntentSender: IntentSender? = null, + +    /** +     * Contains the text content to share supplied by the source app. +     * +     * TODO: Constrain length? +     */ +    val sharedText: CharSequence? = null, + +    /** +     * Supplied to +     * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to +     * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType] +     * are considered for matching share shortcuts currently. +     */ +    val shareTargetFilter: IntentFilter? = null, + +    /** A URI for additional content */ +    val additionalContentUri: Uri? = null, + +    /** Focused item index (from target intent's STREAM_EXTRA) */ +    val focusedItemPosition: Int = 0, + +    /** Value for [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT] on the incoming chooser intent. */ +    val contentTypeHint: ContentTypeHint = ContentTypeHint.NONE, + +    /** +     * Metadata to be shown to the user as a part of the sharesheet window. +     * +     * Specified by the [Intent.EXTRA_METADATA_TEXT] +     */ +    val metadataText: CharSequence? = null, +) { +    val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority + +    fun getReferrerFillInIntent(): Intent { +        return Intent().apply { +            referrerPackage?.also { pkg -> +                putExtra(EXTRA_REFERRER, Uri.parse("$ANDROID_APP_SCHEME://$pkg")) +            } +        } +    } + +    val payloadIntents = listOf(targetIntent) + additionalTargets + +    /** Constructs an instance from only the required values. */ +    constructor( +        targetIntent: Intent, +        launchedFromPackage: String, +        referrer: Uri? +    ) : this( +        targetIntent = targetIntent, +        targetAction = targetIntent.action, +        isSendActionTarget = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), +        targetType = targetIntent.type, +        launchedFromPackage = launchedFromPackage, +        referrer = referrer +    ) +} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt new file mode 100644 index 00000000..a4f74ca9 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ui.model + +import android.content.Intent +import android.content.pm.ResolveInfo +import android.os.UserHandle +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.ext.isHomeIntent + +/** All of the things that are consumed from an incoming Intent Resolution request (+Extras). */ +data class ResolverRequest( +    /** The intent to be resolved to a target. */ +    val intent: Intent, + +    /** +     * Supplied by the system to indicate which profile should be selected by default. This is +     * required since ResolverActivity may be launched as either the originating OR target user when +     * resolving a cross profile intent. +     * +     * Valid values are: [PERSONAL][Profile.Type.PERSONAL] and [WORK][Profile.Type.WORK] and null +     * when the intent is not a forwarded cross-profile intent. +     */ +    val selectedProfile: Profile.Type?, + +    /** +     * When handing a cross profile forwarded intent, this is the user which started the original +     * intent. This is required to allow ResolverActivity to be launched as the target user under +     * some conditions. +     */ +    val callingUser: UserHandle?, + +    /** +     * Indicates if resolving actions for a connected device which has audio capture capability +     * (e.g. is a USB Microphone). +     * +     * When used to handle a connected device, ResolverActivity uses this signal to present a +     * warning when a resolved application does not hold the RECORD_AUDIO permission. (If selected +     * the app would be able to capture audio directly via the device, bypassing audio API +     * permissions.) +     */ +    val isAudioCaptureDevice: Boolean = false, + +    /** A list of a resolved activity targets. This list overrides normal intent resolution. */ +    val resolutionList: List<ResolveInfo>? = null, + +    /** A customized title for the resolver interface. */ +    val title: String? = null, +) { +    val isResolvingHome = intent.isHomeIntent() + +    /** For compatibility with existing code shared between chooser/resolver. */ +    val payloadIntents: List<Intent> = listOf(intent) +} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt b/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt new file mode 100644 index 00000000..e13ef101 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ui.model + +enum class ShareAction { +    SYSTEM_COPY, +    SYSTEM_EDIT, +    APPLICATION_DEFINED +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt new file mode 100644 index 00000000..91eed408 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.ui.viewmodel + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.EXTRA_ALTERNATE_INTENTS +import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS +import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION +import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_TARGETS +import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER +import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS +import android.content.Intent.EXTRA_INITIAL_INTENTS +import android.content.Intent.EXTRA_INTENT +import android.content.Intent.EXTRA_METADATA_TEXT +import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS +import android.content.Intent.EXTRA_TEXT +import android.content.Intent.EXTRA_TITLE +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT +import android.content.IntentFilter +import android.content.IntentSender +import android.net.Uri +import android.os.Bundle +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import com.android.intentresolver.ChooserActivity +import com.android.intentresolver.ContentTypeHint +import com.android.intentresolver.R +import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.util.hasValidIcon +import com.android.intentresolver.v2.ext.hasSendAction +import com.android.intentresolver.v2.ext.ifMatch +import com.android.intentresolver.v2.ui.model.ActivityModel +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.Validation +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.types.IntentOrUri +import com.android.intentresolver.v2.validation.types.array +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +private const val MAX_CHOOSER_ACTIONS = 5 +private const val MAX_INITIAL_INTENTS = 2 + +internal fun Intent.maybeAddSendActionFlags() = +    ifMatch(Intent::hasSendAction) { +        addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) +        addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) +    } + +fun readChooserRequest( +    launch: ActivityModel, +    flags: ChooserServiceFlags +): ValidationResult<ChooserRequest> { +    val extras = launch.intent.extras ?: Bundle() +    @Suppress("DEPRECATION") +    return validateFrom(extras::get) { +        val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags() + +        val isSendAction = targetIntent.hasSendAction() + +        val additionalTargets = readAlternateIntents() ?: emptyList() + +        val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS)) + +        val (customTitle, defaultTitleResource) = +            if (isSendAction) { +                ignored( +                    value<CharSequence>(EXTRA_TITLE), +                    "deprecated in P. You may wish to set a preview title by using EXTRA_TITLE " + +                        "property of the wrapped EXTRA_INTENT." +                ) +                null to R.string.chooseActivity +            } else { +                val custom = optional(value<CharSequence>(EXTRA_TITLE)) +                custom to (custom?.let { 0 } ?: R.string.chooseActivity) +            } + +        val initialIntents = +            optional(array<Intent>(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map { +                it.maybeAddSendActionFlags() +            } +                ?: emptyList() + +        val chosenComponentSender = +            optional(value<IntentSender>(EXTRA_CHOOSER_RESULT_INTENT_SENDER)) +                ?: optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER)) + +        val refinementIntentSender = +            optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + +        val filteredComponents = +            optional(array<ComponentName>(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList() + +        @Suppress("DEPRECATION") +        val callerChooserTargets = +            optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) ?: emptyList() + +        val retainInOnStop = +            optional(value<Boolean>(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false + +        val sharedText = optional(value<CharSequence>(EXTRA_TEXT)) + +        val chooserActions = readChooserActions() ?: emptyList() + +        val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) + +        val additionalContentUri: Uri? +        val focusedItemPos: Int +        if (isSendAction && flags.chooserPayloadToggling()) { +            additionalContentUri = optional(value<Uri>(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI)) +            focusedItemPos = optional(value<Int>(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION)) ?: 0 +        } else { +            additionalContentUri = null +            focusedItemPos = 0 +        } + +        val contentTypeHint = +            if (flags.chooserAlbumText()) { +                when (optional(value<Int>(Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT))) { +                    Intent.CHOOSER_CONTENT_TYPE_ALBUM -> ContentTypeHint.ALBUM +                    else -> ContentTypeHint.NONE +                } +            } else { +                ContentTypeHint.NONE +            } + +        val metadataText = +            if (flags.enableSharesheetMetadataExtra()) { +                optional(value<CharSequence>(EXTRA_METADATA_TEXT)) +            } else { +                null +            } + +        ChooserRequest( +            targetIntent = targetIntent, +            targetAction = targetIntent.action, +            isSendActionTarget = isSendAction, +            targetType = targetIntent.type, +            launchedFromPackage = +                requireNotNull(launch.launchedFromPackage) { +                    "launch.fromPackage was null, See Activity.getLaunchedFromPackage()" +                }, +            title = customTitle, +            defaultTitleResource = defaultTitleResource, +            referrer = launch.referrer, +            filteredComponentNames = filteredComponents, +            callerChooserTargets = callerChooserTargets, +            chooserActions = chooserActions, +            modifyShareAction = modifyShareAction, +            shouldRetainInOnStop = retainInOnStop, +            additionalTargets = additionalTargets, +            replacementExtras = replacementExtras, +            initialIntents = initialIntents, +            chosenComponentSender = chosenComponentSender, +            refinementIntentSender = refinementIntentSender, +            sharedText = sharedText, +            shareTargetFilter = targetIntent.toShareTargetFilter(), +            additionalContentUri = additionalContentUri, +            focusedItemPosition = focusedItemPos, +            contentTypeHint = contentTypeHint, +            metadataText = metadataText, +        ) +    } +} + +fun Validation.readAlternateIntents(): List<Intent>? = +    optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } + +fun Validation.readChooserActions(): List<ChooserAction>? = +    optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS)) +        ?.filter { hasValidIcon(it) } +        ?.take(MAX_CHOOSER_ACTIONS) + +private fun Intent.toShareTargetFilter(): IntentFilter? { +    return type?.let { +        IntentFilter().apply { +            action?.also { addAction(it) } +            addDataType(it) +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt new file mode 100644 index 00000000..8ed2fa29 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.ui.viewmodel + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.v2.ui.model.ActivityModel +import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.log +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +private const val TAG = "ChooserViewModel" + +@HiltViewModel +class ChooserViewModel +@Inject +constructor( +    args: SavedStateHandle, +    flags: ChooserServiceFlags, +) : ViewModel() { + +    /** Parcelable-only references provided from the creating Activity */ +    val activityModel: ActivityModel = +        requireNotNull(args[ACTIVITY_MODEL_KEY]) { +            "ActivityModel missing in SavedStateHandle! ($ACTIVITY_MODEL_KEY)" +        } + +    /** The result of reading and validating the inputs provided in savedState. */ +    private val status: ValidationResult<ChooserRequest> = readChooserRequest(activityModel, flags) + +    val chooserRequest: ChooserRequest by lazy { +        when (status) { +            is Valid -> status.value +            is Invalid -> error(status.errors) +        } +    } + +    fun init(): Boolean { +        Log.i(TAG, "viewModel init") +        if (status is Invalid) { +            status.errors.forEach { finding -> finding.log(TAG) } +            return false +        } +        Log.i(TAG, "request = $chooserRequest") +        return true +    } +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt new file mode 100644 index 00000000..bbc376ea --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.ui.viewmodel + +import android.os.Bundle +import android.os.UserHandle +import com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL +import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK +import com.android.intentresolver.v2.shared.model.Profile +import com.android.intentresolver.v2.ui.model.ActivityModel +import com.android.intentresolver.v2.ui.model.ResolverRequest +import com.android.intentresolver.v2.validation.Validation +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +const val EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER" +const val EXTRA_SELECTED_PROFILE = +    "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE" +const val EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device" + +fun readResolverRequest(launch: ActivityModel): ValidationResult<ResolverRequest> { +    @Suppress("DEPRECATION") +    return validateFrom((launch.intent.extras ?: Bundle())::get) { +        val callingUser = optional(value<UserHandle>(EXTRA_CALLING_USER)) +        val selectedProfile = checkSelectedProfile() +        val audioDevice = optional(value<Boolean>(EXTRA_IS_AUDIO_CAPTURE_DEVICE)) ?: false +        ResolverRequest(launch.intent, selectedProfile, callingUser, audioDevice) +    } +} + +private fun Validation.checkSelectedProfile(): Profile.Type? { +    return when (val selected = optional(value<Int>(EXTRA_SELECTED_PROFILE))) { +        null -> null +        PROFILE_PERSONAL -> Profile.Type.PERSONAL +        PROFILE_WORK -> Profile.Type.WORK +        else -> +            error( +                EXTRA_SELECTED_PROFILE + +                    " has invalid value ($selected)." + +                    " Must be either ResolverActivity.PROFILE_PERSONAL ($PROFILE_PERSONAL)" + +                    " or ResolverActivity.PROFILE_WORK ($PROFILE_WORK)." +            ) +    } +} diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt index 9a3cc9c7..bdf2f00a 100644 --- a/java/src/com/android/intentresolver/v2/validation/Findings.kt +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -34,9 +34,13 @@ val Finding.logcatPriority      get() =          when (importance) {              CRITICAL -> Log.ERROR -            else -> Log.WARN +            WARNING -> Log.WARN          } +fun Finding.log(tag: String) { +    Log.println(logcatPriority, tag, message) +} +  private fun formatMessage(key: String? = null, msg: String) = buildString {      key?.also { append("['$key']: ") }      append(msg) @@ -52,18 +56,21 @@ data class IgnoredValue(          get() = formatMessage(key, "Ignored. $reason")  } -data class RequiredValueMissing( +data class NoValue(      val key: String, +    override val importance: Importance,      val allowedType: KClass<*>,  ) : Finding { -    override val importance = CRITICAL -      override val message: String          get() =              formatMessage(                  key, -                "expected value of ${allowedType.simpleName}, " + "but no value was present" +                if (importance == CRITICAL) { +                    "expected value of ${allowedType.simpleName}, " + "but no value was present" +                } else { +                    "no ${allowedType.simpleName} value present" +                }              )  } diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt index 46939602..6072ec9f 100644 --- a/java/src/com/android/intentresolver/v2/validation/Validation.kt +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -90,7 +90,7 @@ fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): Va                      is InvalidResultError -> Invalid(validation.findings)                      // Some other exception was thrown from [validate], -                    else -> Invalid(findings = listOf(UncaughtException(it))) +                    else -> Invalid(error = UncaughtException(it))                  }              }          ) @@ -107,8 +107,8 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation {      override fun <T> ignored(property: Validator<T>, reason: String) {          val result = property.validate(source, WARNING) -        if (result.value != null) { -            // Note: Any findings about the value (result.findings) are ignored. +        if (result is Valid) { +            // Note: Any warnings about the value itself (result.findings) are ignored.              findings += IgnoredValue(property.key, reason)          }      } @@ -117,8 +117,16 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation {          return runCatching { property.validate(source, importance) }              .fold(                  onSuccess = { result -> -                    findings += result.findings -                    result.value +                    return when (result) { +                        is Valid -> { +                            findings += result.warnings +                            result.value +                        } +                        is Invalid -> { +                            findings += result.errors +                            null +                        } +                    }                  },                  onFailure = {                      findings += UncaughtException(it, property.key) diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt index 092cabe8..f5c467dc 100644 --- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -15,25 +15,12 @@   */  package com.android.intentresolver.v2.validation -import android.util.Log +sealed interface ValidationResult<T> -sealed interface ValidationResult<T> { -    val value: T? -    val findings: List<Finding> - -    fun isSuccess() = value != null - -    fun getOrThrow(): T = -        checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } - -    fun <T> reportToLogcat(tag: String) { -        findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } -    } +data class Valid<T>(val value: T, val warnings: List<Finding> = emptyList()) : ValidationResult<T> { +    constructor(value: T, warning: Finding) : this(value, listOf(warning))  } -data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) : -    ValidationResult<T> - -data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> { -    override val value: T? = null +data class Invalid<T>(val errors: List<Finding> = emptyList()) : ValidationResult<T> { +    constructor(error: Finding) : this(listOf(error))  } diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt index 3cefeb15..050bd895 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -18,7 +18,8 @@ package com.android.intentresolver.v2.validation.types  import android.content.Intent  import android.net.Uri  import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue  import com.android.intentresolver.v2.validation.Valid  import com.android.intentresolver.v2.validation.ValidationResult  import com.android.intentresolver.v2.validation.Validator @@ -40,12 +41,14 @@ class IntentOrUri(override val key: String) : Validator<Intent> {              is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME))              // No value present. -            null -> createResult(importance, RequiredValueMissing(key, Intent::class)) +            null -> when (importance) { +                Importance.WARNING -> Invalid() // No warnings if optional, but missing +                Importance.CRITICAL -> Invalid(NoValue(key, importance, Intent::class)) +            }              // Some other type.              else -> { -                return createResult( -                    importance, +                return Invalid(                      ValueIsWrongType(                          key,                          importance, diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt index c6c4abba..78adfd36 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -15,8 +15,10 @@   */  package com.android.intentresolver.v2.validation.types +import android.content.Intent  import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue  import com.android.intentresolver.v2.validation.Valid  import com.android.intentresolver.v2.validation.ValidationResult  import com.android.intentresolver.v2.validation.Validator @@ -37,8 +39,10 @@ class ParceledArray<T : Any>(          return when (val value: Any? = source(key)) {              // No value present. -            null -> createResult(importance, RequiredValueMissing(key, elementType)) - +            null -> when (importance) { +                Importance.WARNING -> Invalid() // No warnings if optional, but missing +                Importance.CRITICAL -> Invalid(NoValue(key, importance, elementType)) +            }              // A parcel does not transfer the element type information for parcelable              // arrays. This leads to a restored type of Array<Parcelable>, which is              // incompatible with Array<T : Parcelable>. @@ -54,8 +58,7 @@ class ParceledArray<T : Any>(                      // At least one incorrect element type found.                      else -> -                        createResult( -                            importance, +                        Invalid(                              WrongElementType(                                  key,                                  importance, @@ -69,8 +72,7 @@ class ParceledArray<T : Any>(              // The value is not an Array at all.              else -> -                createResult( -                    importance, +                Invalid(                      ValueIsWrongType(                          key,                          importance, diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt index 3287b84b..0105541d 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -16,7 +16,8 @@  package com.android.intentresolver.v2.validation.types  import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue  import com.android.intentresolver.v2.validation.Valid  import com.android.intentresolver.v2.validation.ValidationResult  import com.android.intentresolver.v2.validation.Validator @@ -36,19 +37,21 @@ class SimpleValue<T : Any>(              expected.isInstance(value) -> return Valid(expected.cast(value))              // No value is present. -            value == null -> createResult(importance, RequiredValueMissing(key, expected)) +            value == null -> when (importance) { +                Importance.WARNING -> Invalid() // No warnings if optional, but missing +                Importance.CRITICAL -> Invalid(NoValue(key, importance, expected)) +            }              // The value is some other type.              else -> -                createResult( -                    importance, +                Invalid(listOf(                      ValueIsWrongType(                          key,                          importance,                          actualType = value::class,                          allowedTypes = listOf(expected)                      ) -                ) +                ))          }      }  } diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt index 4e6e5dff..70993b4d 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt @@ -15,13 +15,6 @@   */  package com.android.intentresolver.v2.validation.types -import com.android.intentresolver.v2.validation.Finding -import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.Importance.CRITICAL -import com.android.intentresolver.v2.validation.Importance.WARNING -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult  import com.android.intentresolver.v2.validation.Validator  inline fun <reified T : Any> value(key: String): Validator<T> { @@ -31,15 +24,3 @@ inline fun <reified T : Any> value(key: String): Validator<T> {  inline fun <reified T : Any> array(key: String): Validator<List<T>> {      return ParceledArray(key, T::class)  } - -/** - * Convenience function to wrap a finding in an appropriate result type. - * - * An error [finding] is suppressed when [importance] == [WARNING] - */ -internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> { -    return when (importance) { -        WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING }) -        CRITICAL -> Invalid(listOf(finding)) -    } -} diff --git a/java/src/com/android/intentresolver/widget/BadgeTextView.kt b/java/src/com/android/intentresolver/widget/BadgeTextView.kt new file mode 100644 index 00000000..b6cadd86 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/BadgeTextView.kt @@ -0,0 +1,88 @@ +package com.android.intentresolver.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.widget.TextView + +/** + * A TextView that supports a badge at the end of the text. If the text, when centered in the view, + * leaves enough room for the badge, the badge is just displayed at the end of the view. Otherwise, + * the necessary amount of space for the badge is reserved and the text gets centered in the + * remaining free space. + */ +class BadgeTextView : TextView { +    constructor(context: Context) : this(context, null) + +    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + +    constructor( +        context: Context, +        attrs: AttributeSet?, +        defStyleAttr: Int +    ) : this(context, attrs, defStyleAttr, 0) + +    constructor( +        context: Context?, +        attrs: AttributeSet?, +        defStyleAttr: Int, +        defStyleRes: Int +    ) : super(context, attrs, defStyleAttr, defStyleRes) { +        super.setGravity(Gravity.CENTER) +        defaultPaddingLeft = paddingLeft +        defaultPaddingRight = paddingRight +    } + +    private var defaultPaddingLeft = 0 +    private var defaultPaddingRight = 0 + +    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { +        super.setPadding(left, top, right, bottom) +        defaultPaddingLeft = paddingLeft +        defaultPaddingRight = paddingRight +    } + +    override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { +        super.setPaddingRelative(start, top, end, bottom) +        defaultPaddingLeft = paddingLeft +        defaultPaddingRight = paddingRight +    } + +    /** Sets end-sided badge. */ +    var badgeDrawable: Drawable? = null +        set(value) { +            if (field !== value) { +                field = value +                super.setBackground(value) +            } +        } + +    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { +        super.setPadding(defaultPaddingLeft, paddingTop, defaultPaddingRight, paddingBottom) +        super.onMeasure(widthMeasureSpec, heightMeasureSpec) +        val badge = badgeDrawable ?: return +        if (badge.intrinsicWidth <= paddingEnd) return +        var maxLineWidth = 0f +        for (i in 0 until layout.lineCount) { +            maxLineWidth = maxOf(maxLineWidth, layout.getLineWidth(i)) +        } +        val sideSpace = (measuredWidth - maxLineWidth) / 2 +        if (sideSpace < badge.intrinsicWidth) { +            super.setPaddingRelative( +                paddingStart, +                paddingTop, +                paddingEnd + badge.intrinsicWidth - sideSpace.toInt(), +                paddingBottom +            ) +            super.onMeasure(widthMeasureSpec, heightMeasureSpec) +        } +    } + +    override fun setBackground(background: Drawable?) { +        badgeDrawable = null +        super.setBackground(background) +    } + +    override fun setGravity(gravity: Int): Unit = error("Not supported") +}  |