diff options
Diffstat (limited to 'java')
270 files changed, 12611 insertions, 14044 deletions
diff --git a/java/res/drawable/chooser_direct_share_label_placeholder.xml b/java/res/drawable/chooser_direct_share_label_placeholder.xml deleted file mode 100644 index b21444bf..00000000 --- a/java/res/drawable/chooser_direct_share_label_placeholder.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -  ~ Copyright (C) 2019 The Android Open Source Project -  ~ -  ~ Licensed under the Apache License, Version 2.0 (the "License"); -  ~ you may not use this file except in compliance with the License. -  ~ You may obtain a copy of the License at -  ~ -  ~      http://www.apache.org/licenses/LICENSE-2.0 -  ~ -  ~ Unless required by applicable law or agreed to in writing, software -  ~ distributed under the License is distributed on an "AS IS" BASIS, -  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -  ~ See the License for the specific language governing permissions and -  ~ limitations under the License -  --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - -    <!-- This drawable is intended to be used as the background of a two line TextView. We only -         want the height to be ~1 line. Do this cheaply by applying padding to the bottom. --> -    <item android:bottom="18dp"> -        <shape android:shape="rectangle" > - -            <!-- Size used for scaling should the container be different dimensions --> -            <size android:width="@dimen/chooser_direct_share_label_placeholder_max_width" -                  android:height="18dp"/> - -            <!-- Absurd corner radius to ensure pill shape --> -            <corners android:bottomLeftRadius="100dp" -                     android:bottomRightRadius="100dp" -                     android:topLeftRadius="100dp" -                     android:topRightRadius="100dp" /> - -            <solid android:color="@color/chooser_gradient_background "/> -        </shape> -    </item> -</layer-list>
\ No newline at end of file diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 3c836b4c..90832d23 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -26,7 +26,13 @@      android:orientation="vertical"      android:background="?androidprv:attr/materialColorSurfaceContainer"> -    <include layout="@layout/chooser_headline_row"/> +    <ViewStub +        android:id="@+id/chooser_headline_row_stub" +        android:layout="@layout/chooser_headline_row" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:paddingHorizontal="@dimen/chooser_edge_margin_normal" +        android:layout_marginBottom="@dimen/chooser_view_spacing" />      <RelativeLayout          android:layout_width="match_parent" diff --git a/java/res/layout/chooser_grid_preview_files_text.xml b/java/res/layout/chooser_grid_preview_files_text.xml index c64d7ddd..e7747496 100644 --- a/java/res/layout/chooser_grid_preview_files_text.xml +++ b/java/res/layout/chooser_grid_preview_files_text.xml @@ -25,7 +25,13 @@      android:orientation="vertical"      android:background="?androidprv:attr/materialColorSurfaceContainer"> -    <include layout="@layout/chooser_headline_row" /> +    <ViewStub +        android:id="@+id/chooser_headline_row_stub" +        android:layout="@layout/chooser_headline_row" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:paddingHorizontal="@dimen/chooser_edge_margin_normal" +        android:layout_marginBottom="@dimen/chooser_view_spacing" />      <LinearLayout          android:layout_width="match_parent" diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 4a832324..4745e04c 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -26,7 +26,13 @@      android:importantForAccessibility="no"      android:background="?androidprv:attr/materialColorSurfaceContainer"> -    <include layout="@layout/chooser_headline_row"/> +    <ViewStub +        android:id="@+id/chooser_headline_row_stub" +        android:layout="@layout/chooser_headline_row" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:paddingHorizontal="@dimen/chooser_edge_margin_normal" +        android:layout_marginBottom="@dimen/chooser_view_spacing" />      <com.android.intentresolver.widget.ScrollableImagePreviewView          android:id="@+id/scrollable_image_preview" diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml index df906cce..f3045c34 100644 --- a/java/res/layout/chooser_grid_preview_text.xml +++ b/java/res/layout/chooser_grid_preview_text.xml @@ -27,7 +27,13 @@      android:orientation="vertical"      android:background="?androidprv:attr/materialColorSurfaceContainer"> -  <include layout="@layout/chooser_headline_row" /> +  <ViewStub +      android:id="@+id/chooser_headline_row_stub" +      android:layout="@layout/chooser_headline_row" +      android:layout_width="match_parent" +      android:layout_height="wrap_content" +      android:paddingHorizontal="@dimen/chooser_edge_margin_normal" +      android:layout_marginBottom="@dimen/chooser_view_spacing" />    <androidx.constraintlayout.widget.ConstraintLayout        android:layout_width="match_parent" diff --git a/java/res/layout/chooser_grid_scrollable_preview.xml b/java/res/layout/chooser_grid_scrollable_preview.xml new file mode 100644 index 00000000..c1bcf912 --- /dev/null +++ b/java/res/layout/chooser_grid_scrollable_preview.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +* Copyright 2015, The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*     http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +--> +<com.android.intentresolver.widget.ResolverDrawerLayout +    xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:layout_gravity="center" +    app:maxCollapsedHeight="0dp" +    app:maxCollapsedHeightSmall="56dp" +    app:useScrollablePreviewNestedFlingLogic="true" +    android:maxWidth="@dimen/chooser_width" +    android:id="@androidprv:id/contentPanel"> + +    <RelativeLayout +        android:id="@androidprv:id/chooser_header" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        app:layout_alwaysShow="true" +        android:elevation="0dp" +        android:background="@drawable/bottomsheet_background"> + +        <View +            android:id="@androidprv:id/drag" +            android:layout_width="64dp" +            android:layout_height="4dp" +            android:background="@drawable/ic_drag_handle" +            android:layout_marginTop="@dimen/chooser_edge_margin_thin" +            android:layout_marginBottom="@dimen/chooser_edge_margin_thin" +            android:layout_centerHorizontal="true" +            android:layout_alignParentTop="true" /> + +        <TextView android:id="@android:id/title" +                  android:layout_height="wrap_content" +                  android:layout_width="wrap_content" +                  android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle" +                  android:gravity="center" +                  android:paddingBottom="@dimen/chooser_view_spacing" +                  android:paddingLeft="24dp" +                  android:paddingRight="24dp" +                  android:visibility="gone" +                  android:layout_below="@androidprv:id/drag" +                  android:layout_centerHorizontal="true"/> +    </RelativeLayout> + +    <FrameLayout +        android:id="@+id/chooser_headline_row_container" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        app:layout_alwaysShow="true" +        android:background="?androidprv:attr/materialColorSurfaceContainer"> + +        <ViewStub +            android:id="@+id/chooser_headline_row_stub" +            android:inflatedId="@+id/chooser_headline_row" +            android:layout="@layout/chooser_headline_row" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:paddingHorizontal="@dimen/chooser_edge_margin_normal" +            android:layout_marginBottom="@dimen/chooser_view_spacing" /> +    </FrameLayout> + +    <com.android.intentresolver.widget.ChooserNestedScrollView +        android:layout_width="match_parent" +        android:layout_height="wrap_content"> + +        <LinearLayout +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:orientation="vertical"> + +            <FrameLayout +                android:id="@androidprv:id/content_preview_container" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:visibility="gone" /> + +            <TabHost +                android:id="@androidprv:id/profile_tabhost" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_alignParentTop="true" +                android:layout_centerHorizontal="true" +                android:background="?androidprv:attr/materialColorSurfaceContainer"> +                <LinearLayout +                    android:orientation="vertical" +                    android:layout_width="match_parent" +                    android:layout_height="wrap_content"> +                    <TabWidget +                        android:id="@android:id/tabs" +                        android:layout_width="match_parent" +                        android:layout_height="wrap_content" +                        android:visibility="gone"> +                    </TabWidget> +                    <FrameLayout +                        android:id="@android:id/tabcontent" +                        android:layout_width="match_parent" +                        android:layout_height="wrap_content"> +                        <com.android.intentresolver.ResolverViewPager +                            android:id="@androidprv:id/profile_pager" +                            android:layout_width="match_parent" +                            android:layout_height="wrap_content"/> +                    </FrameLayout> +                </LinearLayout> +            </TabHost> +    </LinearLayout> + +    </com.android.intentresolver.widget.ChooserNestedScrollView> + +</com.android.intentresolver.widget.ResolverDrawerLayout> diff --git a/java/res/layout/chooser_list_per_profile_wrap.xml b/java/res/layout/chooser_list_per_profile_wrap.xml new file mode 100644 index 00000000..157fa75d --- /dev/null +++ b/java/res/layout/chooser_list_per_profile_wrap.xml @@ -0,0 +1,42 @@ +<!-- +  ~ Copyright (C) 2019 The Android Open Source Project +  ~ +  ~ Licensed under the Apache License, Version 2.0 (the "License"); +  ~ you may not use this file except in compliance with the License. +  ~ You may obtain a copy of the License at +  ~ +  ~      http://www.apache.org/licenses/LICENSE-2.0 +  ~ +  ~ Unless required by applicable law or agreed to in writing, software +  ~ distributed under the License is distributed on an "AS IS" BASIS, +  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +  ~ See the License for the specific language governing permissions and +  ~ limitations under the License. +  --> +<RelativeLayout +    xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:descendantFocusability="blocksDescendants"> +    <!--    ^^^ Block descendants from receiving focus to prevent NestedScrollView +    (ChooserNestedScrollView) scrolling to the focused view when switching tabs. Without it, TabHost +    view will request focus on the newly activated tab. The RecyclerView from this layout gets +    focused and  notifies its parents (including NestedScrollView) about it through +    #requestChildFocus method call. NestedScrollView's view implementation of the method  will +    scroll to the focused view. --> + +    <androidx.recyclerview.widget.RecyclerView +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager" +        android:id="@androidprv:id/resolver_list" +        android:clipToPadding="false" +        android:background="?androidprv:attr/materialColorSurfaceContainer" +        android:scrollbars="none" +        android:elevation="1dp" +        android:nestedScrollingEnabled="true" /> + +    <include layout="@layout/resolver_empty_states" /> +</RelativeLayout> diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml index 91b9e041..e0a73836 100644 --- a/java/res/values-af/strings.xml +++ b/java/res/values-af/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Speld <xliff:g id="LABEL">%1$s</xliff:g> vas"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Ontspeld <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Wysig"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # lêer}other{{file_name} + # lêers}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # lêer}other{+ # lêers}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ nog # lêer}other{+ nog # lêers}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deel tans teks"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deel tans skakel"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deel tans prent}other{Deel tans # prente}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deel tans video}other{Deel tans # video’s}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deel tans # item}other{Deel tans # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deel tans prent met teks"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deel prent met skakel"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deel tans # lêer}other{Deel tans # lêers}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deel tans prent met teks}other{Deel tans # prente met teks}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deel tans prent met skakel}other{Deel tans # prente met skakel}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deel tans video met teks}other{Deel tans # video’s met teks}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Prentvoorskouminiprent"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Videovoorskouminiprent"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Lêervoorskouminiprent"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Geen mense om mee te deel is aanbeveel nie"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Programmelys"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hierdie inhoud kan nie met persoonlike programme gedeel word nie"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hierdie inhoud kan nie met persoonlike apps gedeel word nie"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Hierdie inhoud kan nie met persoonlike programme oopgemaak word nie"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Werkprofiel is onderbreek"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tik om aan te skakel"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werkapps word onderbreek"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hervat"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Geen werkprogramme nie"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Geen persoonlike programme nie"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Maak <xliff:g id="APP">%s</xliff:g> in jou persoonlike profiel oop?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Sluit teks in"</string>      <string name="exclude_link" msgid="1332778255031992228">"Sluit skakel uit"</string>      <string name="include_link" msgid="827855767220339802">"Sluit skakel in"</string> +    <string name="pinned" msgid="7623664001331394139">"Vasgespeld"</string>  </resources> diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml index 81450120..ba6409fd 100644 --- a/java/res/values-am/strings.xml +++ b/java/res/values-am/strings.xml @@ -53,29 +53,38 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>ን ፒን አድርግ"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ንቀል"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"አርትዕ"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ፋይል}one{{file_name} + # ፋይል}other{{file_name} + # ፋይሎች}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ፋይል}one{+ # ፋይል}other{+ # ፋይሎች}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ተጨማሪ ፋይል}one{+ # ተጨማሪ ፋይል}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{# ምስልን በማጋራት ላይ}other{# ምስሎችን በማጋራት ላይ}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ቪድዮ በማጋራት ላይ}one{# ቪድዮ በማጋራት ላይ}other{# ቪድዮዎችን በማጋራት ላይ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ንጥልን በማጋራት ላይ}one{# ንጥልን በማጋራት ላይ}other{# ንጥሎችን በማጋራት ላይ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ምስልን ከጽሑፍ ጋር በማጋራት ላይ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ምስልን ከአገናኝ ጋር በማጋራት ላይ"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ፋይልን በማጋራት ላይ}one{# ፋይልን በማጋራት ላይ}other{# ፋይሎችን በማጋራት ላይ}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ምስልን ከጽሑፍ ጋር በማጋራት ላይ}one{# ምስልን ከጽሑፍ ጋር በማጋራት ላይ}other{# ምስሎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ምስልን ከአገናኝ ጋር በማጋራት ላይ}one{# ምስልን ከአገናኝ ጋር በማጋራት ላይ}other{# ምስሎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ቪድዮ ከጽሑፍ ጋር በማጋራት ላይ}one{# ቪድዮ ከጽሑፍ ጋር በማጋራት ላይ}other{# ቪድዮዎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"የምስል ቅድመ ዕይታ ጥፍር አከል"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"የቪድዮ ቅድመ ዕይታ ጥፍር አከል"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"የፋይል ቅድመ ዕይታ ጥፍር አከል"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"የሚያጋሯቸው ምንም የሚመከሩ ሰዎች የሉም"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"የመተግበሪያዎች ዝርዝር"</string>      <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_personal_tab_accessibility" msgid="4467784352232582574">"የግል እይታ"</string> -    <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ እይታ"</string> +    <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"የግል ዕይታ"</string> +    <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ ዕይታ"</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>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ይህ ይዘት በግል መተግበሪያዎች መጋራት አይችልም"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ይህ ይዘት በግል መተግበሪያዎች መከፈት አይችልም"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"የሥራ መገለጫ ባለበት ቆሟል"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ለማብራት መታ ያድርጉ"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"የሥራ መተግበሪያዎች ባሉበት ቆመዋል"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ከቆመበት ቀጥል"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ምንም የሥራ መተግበሪያዎች የሉም"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ምንም የግል መተግበሪያዎች የሉም"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> በግል መገለጫዎ ውስጥ ይከፈት?"</string> @@ -83,7 +92,8 @@      <string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"የግል አሳሽ ተጠቀም"</string>      <string name="miniresolver_use_work_browser" msgid="7892699758493230342">"የስራ አሳሽ ተጠቀም"</string>      <string name="exclude_text" msgid="5508128757025928034">"ጽሁፍን አታካትት"</string> -    <string name="include_text" msgid="642280283268536140">"ፅሁፍ ጨምር"</string> +    <string name="include_text" msgid="642280283268536140">"ጽሁፍ ጨምር"</string>      <string name="exclude_link" msgid="1332778255031992228">"አገናኝን አታካትት"</string>      <string name="include_link" msgid="827855767220339802">"አገናኝ አካትት"</string> +    <string name="pinned" msgid="7623664001331394139">"ፒን ተደርጓል"</string>  </resources> diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml index 16bff5bf..da8d4de2 100644 --- a/java/res/values-ar/strings.xml +++ b/java/res/values-ar/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"تثبيت <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"إزالة تثبيت <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"تعديل"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ملف واحد}zero{{file_name} + # ملف}two{{file_name} + ملفان}few{{file_name} + # ملفات}many{{file_name} + # ملفًا}other{{file_name} + # ملف}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ ملف واحد}zero{+ # ملف}two{+ ملفان}few{+ # ملفات}many{+ # ملفًا}other{+ # ملف}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{وملف واحد آخر}zero{و# ملف آخر}two{وملفان آخران}few{و# ملفات أخرى}many{و# ملفًا آخر}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{جارٍ مشاركة صورة واحدة}zero{جارٍ مشاركة # صورة}two{جارٍ مشاركة صورتَين}few{جارٍ مشاركة # صور}many{جارٍ مشاركة # صورة}other{جارٍ مشاركة # صورة}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{جارٍ مشاركة فيديو واحد}zero{جارٍ مشاركة # فيديو}two{جارٍ مشاركة فيديوهَين}few{جارٍ مشاركة # فيديوهات}many{جارٍ مشاركة # فيديو}other{جارٍ مشاركة # فيديو}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{جارٍ مشاركة عنصر واحد}zero{جارٍ مشاركة # عنصر}two{جارٍ مشاركة عنصرَين}few{جارٍ مشاركة # عناصر}many{جارٍ مشاركة # عنصرًا}other{جارٍ مشاركة # عنصر}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"مشاركة صورة بنص"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"مشاركة صورة برابط"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{مشاركة ملف واحد}zero{مشاركة # ملف}two{مشاركة ملفَّين}few{مشاركة # ملفات}many{مشاركة # ملفًّا}other{مشاركة # ملف}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{مشاركة صورة واحدة ونص}zero{مشاركة # صورة ونص}two{مشاركة صورتَين ونص}few{مشاركة # صور ونص}many{مشاركة # صورة ونص}other{مشاركة # صورة ونص}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{مشاركة صورة واحدة ورابط}zero{مشاركة # صورة ورابط}two{مشاركة # صورتَين ورابط}few{مشاركة # صور ورابط}many{مشاركة # صورة ورابط}other{مشاركة # صورة ورابط}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{مشاركة فيديو واحد ونص}zero{مشاركة # فيديو ونص}two{مشاركة فيديوهَين ونص}few{مشاركة # فيديوهات ونص}many{مشاركة # فيديو ونص}other{مشاركة # فيديو ونص}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"صورة مصغّرة لمعاينة صورة"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"صورة مصغّرة لمعاينة فيديو"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"صورة مصغّرة لمعاينة ملف"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ما مِن أشخاص مقترحين للمشاركة معهم."</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"قائمة التطبيقات"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"لا يمكن فتح هذا المحتوى باستخدام تطبيقات العمل."</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"لا يمكن مشاركة هذا المحتوى مع التطبيقات الشخصية."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"لا يمكن فتح هذا المحتوى باستخدام التطبيقات الشخصية."</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"الملف الشخصي للعمل متوقف مؤقتًا."</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"انقر لتفعيل الميزة"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"تطبيقات العمل متوقفة مؤقتًا."</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"إلغاء الإيقاف المؤقت"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ما مِن تطبيقات عمل."</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ما مِن تطبيقات شخصية."</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"هل تريد فتح <xliff:g id="APP">%s</xliff:g> في ملفك الشخصي؟"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"تضمين النص"</string>      <string name="exclude_link" msgid="1332778255031992228">"استثناء الرابط"</string>      <string name="include_link" msgid="827855767220339802">"تضمين الرابط"</string> +    <string name="pinned" msgid="7623664001331394139">"مثبَّت"</string>  </resources> diff --git a/java/res/values-as/strings.xml b/java/res/values-as/strings.xml index cd294ec4..14bd864e 100644 --- a/java/res/values-as/strings.xml +++ b/java/res/values-as/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> পিন কৰক"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ক আনপিন কৰক"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"সম্পাদনা কৰক"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # টা ফাইল}one{{file_name} + # টা ফাইল}other{{file_name} + # টা ফাইল}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # টা ফাইল}one{+ # টা ফাইল}other{+ # টা ফাইল}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{আৰু # টা ফাইল}one{আৰু # টা ফাইল}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{# খন প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{# খন প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{# টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{# টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}one{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}other{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"পাঠেৰে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি হৈছে"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"লিংকৰে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি হৈছে"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{পাঠৰ সৈতে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{লিংকৰ সৈতে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{পাঠৰ সৈতে ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"প্ৰতিচ্ছবিৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ভিডিঅ’ৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ফাইলৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"শ্বেয়াৰ কৰিবলৈ চুপাৰিছ কৰা কোনো লোক নাই"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"এপ্সমূহৰ সূচী"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"এই সমল কৰ্মস্থানৰ এপৰ জৰিয়তে খুলিব নোৱাৰি"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"এই সমল ব্যক্তিগত এপৰ সৈতে শ্বেয়াৰ কৰিব নোৱাৰি"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"এই সমল ব্যক্তিগত এপৰ জৰিয়তে খুলিব নোৱাৰি"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"কৰ্মস্থানৰ প্ৰ\'ফাইলটো পজ কৰা আছে"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"অন কৰিবলৈ টিপক"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"কাম সম্পর্কীয় এপ্ পজ কৰা আছে"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"আনপজ কৰক"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"কোনো কৰ্মস্থানৰ এপ্ নাই"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"কোনো ব্যক্তিগত এপ্ নাই"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপোনাৰ ব্যক্তিগত প্ৰ’ফাইলত <xliff:g id="APP">%s</xliff:g> খুলিবনে?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"পাঠ অন্তৰ্ভুক্ত কৰক"</string>      <string name="exclude_link" msgid="1332778255031992228">"লিংক বহিৰ্ভূত কৰক"</string>      <string name="include_link" msgid="827855767220339802">"লিংক অন্তৰ্ভুক্ত কৰক"</string> +    <string name="pinned" msgid="7623664001331394139">"পিন কৰা আছে"</string>  </resources> diff --git a/java/res/values-az/strings.xml b/java/res/values-az/strings.xml index 3c66f5c0..a31df362 100644 --- a/java/res/values-az/strings.xml +++ b/java/res/values-az/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Bərkidin: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"İşarələməyin: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Redaktə edin"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fayl}other{{file_name} + # fayl}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fayl}other{+ # fayl}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fayl}other{+ # fayl}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Mətn paylaşılır"</string>      <string name="sharing_link" msgid="2307694372813942916">"Link paylaşılır"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Şəkil paylaşılır}other{# şəkil paylaşılır}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video paylaşılır}other{# video paylaşılır}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# element paylaşılır}other{# element paylaşılır}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Şəkil mətn ilə paylaşılır"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Şəkil link ilə paylaşılır"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fayl paylaşılır}other{# fayl paylaşılır}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Mətn olan şəkil paylaşılır}other{Mətn olan # şəkil paylaşılır}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Link olan şəkil paylaşılır}other{Link olan # şəkil paylaşılır}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Mətn olan video paylaşılır}other{Mətn olan # video paylaşılır}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Şəkil önizləmə miniatürü"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video önizləmə miniatürü"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fayl önizləmə miniatürü"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Paylaşmaq üçün tövsiyə edilən bir kimsə yoxdur"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Tətbiq siyahısı"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontenti iş tətbiqləri ilə açmaq mümkün deyil"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu kontenti şəxsi tətbiqlər ilə paylaşmaq mümkün deyil"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu kontenti şəxsi tətbiqlər ilə açmaq mümkün deyil"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"İş profilinə fasilə verilib"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Aktiv etmək üçün toxunun"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"İş tətbiqləri durdurulub"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Pauzanı bitirin"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"İş tətbiqi yoxdur"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Şəxsi tətbiq yoxdur"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Şəxsi profilinizdə <xliff:g id="APP">%s</xliff:g> tətbiqi açılsın?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Mətn daxil edin"</string>      <string name="exclude_link" msgid="1332778255031992228">"Keçidi istisna edin"</string>      <string name="include_link" msgid="827855767220339802">"Keçid daxil edin"</string> +    <string name="pinned" msgid="7623664001331394139">"Bərkidilib"</string>  </resources> diff --git a/java/res/values-b+sr+Latn/strings.xml b/java/res/values-b+sr+Latn/strings.xml index 83c55e29..ea0d87b3 100644 --- a/java/res/values-b+sr+Latn/strings.xml +++ b/java/res/values-b+sr+Latn/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Zakačite osobu <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Otkači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Izmeni"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fajl}one{{file_name} + # fajl}few{{file_name} + # fajla}other{{file_name} + # fajlova}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Deli se video}one{Deli se # video}few{Dele se # video snimka}other{Deli se # video snimaka}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deli se # stavka}one{Deli se # stavka}few{Dele se # stavke}other{Deli se # stavki}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deli se slika sa tekstom"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deli se slika sa linkom"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deli se slika sa linkom}one{Deli se # slika sa linkom}few{Dele se # slike sa linkom}other{Deli se # slika sa linkom}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deli se video sa tekstom}one{Deli se # video sa tekstom}few{Dele se # video snimka sa tekstom}other{Deli se # videa sa tekstom}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica za pregled slike"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica za pregled videa"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica za pregled fajla"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih ljudi za deljenje"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacija"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj ne može da se otvara pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ovaj sadržaj ne može da se deli pomoću ličnih aplikacija"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ovaj sadržaj ne može da se otvara pomoću ličnih aplikacija"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Poslovni profil je pauziran"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da biste uključili"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovo aktiviraj"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nema poslovnih aplikacija"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nema ličnih aplikacija"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite da na ličnom profilu otvorite: <xliff:g id="APP">%s</xliff:g>?"</string> @@ -84,6 +93,7 @@      <string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Koristi poslovni pregledač"</string>      <string name="exclude_text" msgid="5508128757025928034">"Isključi tekst"</string>      <string name="include_text" msgid="642280283268536140">"Uvrsti tekst"</string> -    <string name="exclude_link" msgid="1332778255031992228">"Isključi link"</string> +    <string name="exclude_link" msgid="1332778255031992228">"Izuzmi link"</string>      <string name="include_link" msgid="827855767220339802">"Uvrsti link"</string> +    <string name="pinned" msgid="7623664001331394139">"Zakačeno"</string>  </resources> diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml index a24b4a36..aecc1cbd 100644 --- a/java/res/values-be/strings.xml +++ b/java/res/values-be/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Замацаваць праграму \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Адмацаваць праграму \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Рэдагаваць"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}one{{file_name} + # файл}few{{file_name} + # файлы}many{{file_name} + # файлаў}other{{file_name} + # файла}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}one{+ # файл}few{+ # файлы}many{+ # файлаў}other{+ # файла}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ яшчэ # файл}one{+ яшчэ # файл}few{+ яшчэ # файлы}many{+ яшчэ # файлаў}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{Абагульванне # відарысаў}many{Абагульванне # відарысаў}other{Абагульванне # відарыса}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Абагульванне відэа}one{Абагульванне # відэа}few{Абагульванне # відэа}many{Абагульванне # відэа}other{Абагульванне # відэа}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Абагульванне # аб\'екта}one{Абагульванне # аб\'екта}few{Абагульванне # аб\'ектаў}many{Абагульванне # аб\'ектаў}other{Абагульванне # аб\'екта}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Абагульванне відарыса з тэкстам"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Абагульванне відарыса са спасылкай"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Абагульваецца # файл}one{Абагульваецца # файл}few{Абагульваюцца # файлы}many{Абагульваюцца # файлаў}other{Абагульваюцца # файла}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Абагульванне відарыса з тэкстам}one{Абагульванне # відарыса з тэкстам}few{Абагульванне # відарысаў з тэкстам}many{Абагульванне # відарысаў з тэкстам}other{Абагульванне # відарыса з тэкстам}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Абагульванне відарыса са спасылкай}one{Абагульванне # відарыса са спасылкай}few{Абагульванне # відарысаў са спасылкай}many{Абагульванне # відарысаў са спасылкай}other{Абагульванне # відарыса са спасылкай}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Абагульванне відэа з тэкстам}one{Абагульванне # відэа з тэкстам}few{Абагульванне # відэа з тэкстам}many{Абагульванне # відэа з тэкстам}other{Абагульванне # відэа з тэкстам}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Мініяцюра перадпрагляду відарыса"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Мініяцюра перадпрагляду відэа"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Мініяцюра перадпрагляду файла"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Няма кантактаў, з якімі рэкамендуецца абагульваць змесціва"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Спіс праграм"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Не ўдалося адкрыць гэта змесціва з дапамогай працоўных праграм"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Не ўдалося абагуліць гэта змесціва з асабістымі праграмамі"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Не ўдалося адкрыць гэта змесціва з дапамогай асабістых праграм"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Працоўны профіль прыпынены"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Націсніце, каб уключыць"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Працоўныя праграмы прыпынены"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Уключыць"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Няма працоўных праграм"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Няма асабістых праграм"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Адкрыць праграму \"<xliff:g id="APP">%s</xliff:g>\" з выкарыстаннем асабістага профілю?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Дадаць тэкст"</string>      <string name="exclude_link" msgid="1332778255031992228">"Выдаліць спасылку"</string>      <string name="include_link" msgid="827855767220339802">"Дадаць спасылку"</string> +    <string name="pinned" msgid="7623664001331394139">"Замацавана"</string>  </resources> diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml index 44892051..5bc22d73 100644 --- a/java/res/values-bg/strings.xml +++ b/java/res/values-bg/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Фиксиране на <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Премахване на фиксирането на <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Редактиране"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файла}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файла}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Видеоклипът се споделя}other{# видеоклипа се споделят}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# елемент се споделя}other{# елемента се споделят}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Изобр. се споделя с текст"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Изобр. се споделя с връзка"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файл се споделя}other{# файла се споделят}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Споделяне на изображението чрез SMS съобщение}other{Споделяне на # изображения чрез SMS съобщение}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Споделяне на изображението чрез връзка}other{Споделяне на # изображения чрез връзка}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Споделяне на видеоклипа чрез SMS съобщение}other{Споделяне на # видеоклипа чрез SMS съобщение}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Миниизображение за визуализация на изображението"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Миниизображение за визуализация на видеоклипа"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Миниизображение за визуализация на файла"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Няма препоръки за хора, с които да споделяте"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Списък с приложения"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Това съдържание не може да се отваря със служебни приложения"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Това съдържание не може да се споделя с лични приложения"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Това съдържание не може да се отваря с лични приложения"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Служебният потребителски профил е поставен на пауза"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Докоснете за включване"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Служебните приложения са поставени на пауза"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Отмяна на паузата"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Няма подходящи служебни приложения"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Няма подходящи лични приложения"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Искате ли да отворите <xliff:g id="APP">%s</xliff:g> в личния си потребителски профил?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Включване на текста"</string>      <string name="exclude_link" msgid="1332778255031992228">"Изключване на връзката"</string>      <string name="include_link" msgid="827855767220339802">"Включване на връзката"</string> +    <string name="pinned" msgid="7623664001331394139">"Фиксирано"</string>  </resources> diff --git a/java/res/values-bn/strings.xml b/java/res/values-bn/strings.xml index 22438fbf..0561cf99 100644 --- a/java/res/values-bn/strings.xml +++ b/java/res/values-bn/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> অ্যাপ পিন করুন"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> অ্যাপ আনপিন করুন"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"এডিট করুন"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ও আরও #টি ফাইল}one{{file_name} ও আরও #টি ফাইল}other{{file_name} ও আরও #টি ফাইল}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{আরও #টি ফাইল}one{আরও #টি ফাইল}other{আরও #টি ফাইল}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{আরও #টি ফাইল}one{আরও #টি ফাইল}other{আরও #টি ফাইল}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"টেক্সট শেয়ার করা হচ্ছে"</string> -    <string name="sharing_link" msgid="2307694372813942916">"লিঙ্ক শেয়ার করা হচ্ছে"</string> +    <string name="sharing_link" msgid="2307694372813942916">"শেয়ার করা লিঙ্ক"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ছবি শেয়ার করা হচ্ছে}one{#টি ছবি শেয়ার করা হচ্ছে}other{#টি ছবি শেয়ার করা হচ্ছে}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ভিডিও শেয়ার করা হচ্ছে}one{#টি ভিডিও শেয়ার করা হচ্ছে}other{#টি ভিডিও শেয়ার করা হচ্ছে}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{#টি আইটেম শেয়ার করা হচ্ছে}one{#টি আইটেম শেয়ার করা হচ্ছে}other{#টি আইটেম শেয়ার করা হচ্ছে}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ছবি টেক্সটের মাধ্যমে শেয়ার করা হচ্ছে"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ছবি লিঙ্কের মাধ্যমে শেয়ার করা হচ্ছে"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{#টি ফাইল শেয়ার করা হচ্ছে}one{#টি ফাইল শেয়ার করা হচ্ছে}other{#টি ফাইল শেয়ার করা হচ্ছে}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{টেক্সট সহ ছবি শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ছবি শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ছবি শেয়ার করা হচ্ছে}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{লিঙ্ক সহ ছবি শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ছবি শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ছবি শেয়ার করা হচ্ছে}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{টেক্সট সহ ভিডিও শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ভিডিও শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ভিডিও শেয়ার করা হচ্ছে}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ছবির প্রিভিউ থাম্বনেল"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ভিডিওর প্রিভিউ থাম্বনেল"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ফাইলের প্রিভিউ থাম্বনেল"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"শেয়ার করার জন্য সাজেস্ট করার মতো কেউ নেই"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"অ্যাপের তালিকা"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"অফিসের অ্যাপে এই খোলা যাবে না"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ব্যক্তিগত অ্যাপে এই কন্টেন্ট শেয়ার করা যাবে না"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ব্যক্তিগত অ্যাপে এই কন্টেন্ট খোলা যাবে না"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"অফিস প্রোফাইল বন্ধ করা আছে"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"চালু করতে ট্যাপ করুন"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"অফিসের অ্যাপ পজ করা আছে"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"আনপজ করুন"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"এর জন্য কোনও অফিস অ্যাপ নেই"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ব্যক্তিগত অ্যাপে দেখা যাবে না"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপনার ব্যক্তিগত প্রোফাইল থেকে <xliff:g id="APP">%s</xliff:g> খুলবেন?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"টেক্সট যোগ করুন"</string>      <string name="exclude_link" msgid="1332778255031992228">"লিঙ্ক বাদ দিন"</string>      <string name="include_link" msgid="827855767220339802">"লিঙ্ক যোগ করুন"</string> +    <string name="pinned" msgid="7623664001331394139">"পিন করা হয়েছে"</string>  </resources> diff --git a/java/res/values-bs/strings.xml b/java/res/values-bs/strings.xml index f4b54c7b..3c88d9c1 100644 --- a/java/res/values-bs/strings.xml +++ b/java/res/values-bs/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Zakači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Otkači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # fajl}one{{file_name} i # fajl}few{{file_name} i # fajla}other{{file_name} i # fajlova}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Dijeljenje teksta"</string>      <string name="sharing_link" msgid="2307694372813942916">"Dijeljenje linka"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Dijeljenje slike}one{Dijeljenje # slike}few{Dijeljenje # slike}other{Dijeljenje # slika}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Dijeljenje videozapisa}one{Dijeljenje # videozapisa}few{Dijeljenje # videozapisa}other{Dijeljenje # videozapisa}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Dijeljenje # stavke}one{Dijeljenje # stavke}few{Dijeljenje # stavke}other{Dijeljenje # stavki}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Dijeljenje slike s tekstom"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Dijeljenje slike s linkom"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Dijeljenje # fajla}one{Dijeljenje # fajla}few{Dijeljenje # fajla}other{Dijeljenje # fajlova}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Dijeljenje slike putem poruke}one{Dijeljenje # slike putem poruke}few{Dijeljenje # slike putem poruke}other{Dijeljenje # slika putem poruke}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Dijeljenje slike putem linka}one{Dijeljenje # slike putem linka}few{Dijeljenje # slike putem linka}other{Dijeljenje # slika putem linka}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Dijeljenje videozapisa putem poruke}one{Dijeljenje # videozapisa putem poruke}few{Dijeljenje # videozapisa putem poruke}other{Dijeljenje # videozapisa putem poruke}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica pregleda slike"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica pregleda videozapisa"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica pregleda fajla"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih osoba s kojima biste dijelili"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacija"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj nije moguće otvoriti pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ovaj sadržaj nije moguće dijeliti pomoću ličnih aplikacija"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ovaj sadržaj nije moguće otvoriti pomoću ličnih aplikacija"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Radni profil je pauziran"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da uključite"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovo pokreni"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nema poslovnih aplikacija"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nema ličnih aplikacija"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na ličnom profilu?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Uključi tekst"</string>      <string name="exclude_link" msgid="1332778255031992228">"Izuzmi link"</string>      <string name="include_link" msgid="827855767220339802">"Uključi link"</string> +    <string name="pinned" msgid="7623664001331394139">"Zakačeno"</string>  </resources> diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml index 97aeeddc..bd0416a5 100644 --- a/java/res/values-ca/strings.xml +++ b/java/res/values-ca/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixa <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"No fixis <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edita"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # fitxer}many{{file_name} i # fitxers}other{{file_name} i # fitxers}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fitxer}many{+ # de fitxers}other{+ # fitxers}}"</string> +    <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 l\'enllaç"</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_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_items" msgid="5266543892527310331">"{count,plural, =1{S\'està compartint # element}many{S\'estan compartint # d\'elements}other{S\'estan compartint # elements}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartint imatge amb text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartint imatge i enllaç"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{S\'està compartint la imatge amb un enllaç}many{S\'estan compartint # d\'imatges amb un enllaç}other{S\'estan compartint # imatges amb un enllaç}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{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_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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de previsualització de la imatge"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de previsualització del vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de previsualització del fitxer"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hi ha cap suggeriment de persones amb qui compartir"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Llista d\'aplicacions"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No es pot obrir aquest contingut amb aplicacions de treball"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"No es pot compartir aquest contingut amb aplicacions personals"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"No es pot obrir aquest contingut amb aplicacions personals"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de treball està en pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toca per activar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les aplicacions de treball estan en pausa"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactiva"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Cap aplicació de treball"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Cap aplicació personal"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vols obrir <xliff:g id="APP">%s</xliff:g> al teu perfil personal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inclou text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclou l\'enllaç"</string>      <string name="include_link" msgid="827855767220339802">"Inclou l\'enllaç"</string> +    <string name="pinned" msgid="7623664001331394139">"Fixat"</string>  </resources> diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml index e15b1b00..a5deed60 100644 --- a/java/res/values-cs/strings.xml +++ b/java/res/values-cs/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Připnout <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Odepnout: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Upravit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # soubor}few{{file_name} + # soubory}many{{file_name} + # souboru}other{{file_name} + # souborů}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # soubor}few{+ # soubory}many{+ # souboru}other{+ # souborů}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{a # další soubor}few{a # další soubory}many{a # dalšího souboru}other{a # dalších souborů}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sdílení textu"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sdílení odkazu"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sdílení obrázku}few{Sdílení # obrázků}many{Sdílení # obrázku}other{Sdílení # obrázků}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sdílení videa}few{Sdílení # videí}many{Sdílení # videa}other{Sdílení # videí}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sdílení # položky}few{Sdílení # položek}many{Sdílení # položky}other{Sdílení # položek}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sdílení obrázku s textem"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sdílení obrázku s odkazem"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sdílení # souboru}few{Sdílení # souborů}many{Sdílení # souboru}other{Sdílení # souborů}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sdílení obrázku s textem}few{Sdílení # obrázků s textem}many{Sdílení # obrázku s textem}other{Sdílení # obrázků s textem}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sdílení obrázku s odkazem}few{Sdílení # obrázků s odkazem}many{Sdílení # obrázku s odkazem}other{Sdílení # obrázků s odkazem}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sdílení videa s textem}few{Sdílení # videí s textem}many{Sdílení # videa s textem}other{Sdílení # videí s textem}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura náhledu obrázku"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura náhledu videa"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura náhledu souboru"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Žádní doporučení lidé, s nimiž můžete sdílet"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Seznam aplikací"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah nelze otevřít pomocí pracovních aplikací"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tento obsah nelze sdílet pomocí osobních aplikací"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tento obsah nelze otevřít pomocí osobních aplikací"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Pracovní profil je pozastaven"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Klepnutím ho zapnete"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pracovní aplikace jsou pozastaveny"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Zrušit pozastavení"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Žádné pracovní aplikace"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Žádné osobní aplikace"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otevřít aplikaci <xliff:g id="APP">%s</xliff:g> v osobním profilu?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Zahrnout text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Vyloučit odkaz"</string>      <string name="include_link" msgid="827855767220339802">"Zahrnout odkaz"</string> +    <string name="pinned" msgid="7623664001331394139">"Připnuto"</string>  </resources> diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml index ef66baeb..8d226d44 100644 --- a/java/res/values-da/strings.xml +++ b/java/res/values-da/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fastgør <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Frigør <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Rediger"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}one{{file_name} + # fil}other{{file_name} + # filer}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}one{+ # fil}other{+ # filer}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fil mere}one{+ # fil mere}other{+ # filer mere}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deler tekst"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deler link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deler billede}one{Deler # billede}other{Deler # billeder}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deler video}one{Deler # video}other{Deler # videoer}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deler # element}one{Deler # element}other{Deler # elementer}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deler billede med tekst"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deler billede med et link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deler # fil}one{Deler # fil}other{Deler # filer}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deler billede med tekst}one{Deler # billede med tekst}other{Deler # billeder med tekst}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deler billede med et link}one{Deler # billede med et link}other{Deler # billeder med et link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deler video med tekst}one{Deler # video med tekst}other{Deler # videoer med tekst}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniaturepreview af billede"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniaturepreview af video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniaturepreview af fil"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Der er ingen anbefalede personer at dele med"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste over apps"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette indhold kan ikke åbnes med arbejdsapps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Dette indhold kan ikke deles med personlige apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Dette indhold kan ikke åbnes med personlige apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Arbejdsprofilen er sat på pause"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tryk for at aktivere"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Dine arbejdsapps er sat på pause"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Genoptag"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Der er ingen arbejdsapps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Der er ingen personlige apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åbne <xliff:g id="APP">%s</xliff:g> på din personlige profil?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inkluder tekst"</string>      <string name="exclude_link" msgid="1332778255031992228">"Ekskluder link"</string>      <string name="include_link" msgid="827855767220339802">"Inkluder link"</string> +    <string name="pinned" msgid="7623664001331394139">"Fastgjort"</string>  </resources> diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml index a78310d5..dc476fa7 100644 --- a/java/res/values-de/strings.xml +++ b/java/res/values-de/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> anpinnen"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> loslösen"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Bearbeiten"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # Datei}other{{file_name} + # Dateien}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # Datei}other{+ # Dateien}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # weitere Datei}other{+ # weitere Dateien}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Text wird geteilt"</string>      <string name="sharing_link" msgid="2307694372813942916">"Link wird geteilt"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Bild wird geteilt}other{# Bilder werden geteilt}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video wird geteilt}other{# Videos werden geteilt}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# Element wird geteilt}other{# Elemente werden geteilt}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Bild mit Text geteilt"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Bild mit Link geteilt"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# Datei wird freigegeben}other{# Dateien werden freigegeben}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Bild wird mit Text geteilt}other{# Bilder werden mit Text geteilt}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bild wird per Link geteilt}other{# Bilder werden per Link geteilt}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Video wird per SMS geteilt}other{# Videos werden per SMS geteilt}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Vorschau-Miniaturansicht für Bild"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Vorschau-Miniaturansicht für Video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Vorschau-Miniaturansicht für Datei"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Keine empfohlenen Empfänger"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste der Apps"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Diese Art von Inhalt kann nicht mit geschäftlichen Apps geöffnet werden"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Diese Art von Inhalt kann nicht über private Apps geteilt werden"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Diese Art von Inhalt kann nicht mit privaten Apps geöffnet werden"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Arbeitsprofil pausiert"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Zum Aktivieren tippen"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Geschäftliche Apps sind pausiert"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Nicht mehr pausieren"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Keine geschäftlichen Apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Keine privaten Apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> in deinem privaten Profil öffnen?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Text einschließen"</string>      <string name="exclude_link" msgid="1332778255031992228">"Link ausschließen"</string>      <string name="include_link" msgid="827855767220339802">"Link einschließen"</string> +    <string name="pinned" msgid="7623664001331394139">"Angepinnt"</string>  </resources> diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml index 31e273ab..e760e00c 100644 --- a/java/res/values-el/strings.xml +++ b/java/res/values-el/strings.xml @@ -44,27 +44,36 @@      <string name="whichImageCaptureApplicationLabel" msgid="987153638235357094">"Λήψη εικόνας"</string>      <string name="use_a_different_app" msgid="2062380818535918975">"Χρήση άλλης εφαρμογής"</string>      <string name="chooseActivity" msgid="6659724877523973446">"Επιλέξτε μια ενέργεια"</string> -    <string name="noApplications" msgid="1139487441772284671">"Δεν υπάρχουν εφαρμογές, οι οποίες μπορούν να εκτελέσουν αυτήν την ενέργεια."</string> -    <string name="forward_intent_to_owner" msgid="6454987608971162379">"Χρησιμοποιείτε αυτήν την εφαρμογή εκτός του προφίλ εργασίας σας"</string> -    <string name="forward_intent_to_work" msgid="2906094223089139419">"Χρησιμοποιείτε αυτήν την εφαρμογή στο προφίλ εργασίας"</string> +    <string name="noApplications" msgid="1139487441772284671">"Δεν υπάρχουν εφαρμογές, οι οποίες μπορούν να εκτελέσουν αυτή την ενέργεια."</string> +    <string name="forward_intent_to_owner" msgid="6454987608971162379">"Χρησιμοποιείτε αυτή την εφαρμογή εκτός του προφίλ εργασίας σας"</string> +    <string name="forward_intent_to_work" msgid="2906094223089139419">"Χρησιμοποιείτε αυτή την εφαρμογή στο προφίλ εργασίας"</string>      <string name="activity_resolver_use_always" msgid="8674194687637555245">"Πάντα"</string>      <string name="activity_resolver_use_once" msgid="594173435998892989">"Μόνο μία φορά"</string>      <string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"Η εφαρμογή <xliff:g id="APP">%1$s</xliff:g> δεν υποστηρίζει προφίλ εργασίας"</string>      <string name="pin_specific_target" msgid="5057063421361441406">"Καρφίτσωμα <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Ξεκαρφίτσωμα <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Επεξεργασία"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # αρχείο}other{{file_name} + # αρχεία}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # αρχείο}other{+ # αρχεία}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Κοινοποίηση βίντεο}other{Κοινοποίηση # βίντεο}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Κοινοποίηση # στοιχείου}other{Κοινοποίηση # στοιχείων}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Κοινοπ. εικόνας με κείμ."</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Κοινοπ. εικόνας με σύνδ."</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Κοινοποίηση εικόνας με σύνδεσμο}other{Κοινοποίηση # εικόνων με σύνδεσμο}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Κοινοποίηση βίντεο με κείμενο}other{Κοινοποίηση # βίντεο με κείμενο}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Μικρογραφία προεπισκόπησης εικόνας"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Μικρογραφία προεπισκόπησης βίντεο"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Μικρογραφία προεπισκόπησης αρχείου"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Δεν υπάρχουν προτεινόμενα άτομα για κοινοποίηση"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Λίστα εφαρμογών"</string> -    <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Δεν έχει εκχωρηθεί άδεια εγγραφής σε αυτήν την εφαρμογή, αλλά μέσω αυτής της συσκευής USB θα μπορεί να εγγράφει ήχο."</string> +    <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_personal_tab_accessibility" msgid="4467784352232582574">"Προσωπική προβολή"</string> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Δεν είναι δυνατό το άνοιγμα αυτού του περιεχομένου με εφαρμογές εργασιών"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Δεν είναι δυνατή η κοινοποίηση αυτού του περιεχομένου με προσωπικές εφαρμογές"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Δεν είναι δυνατό το άνοιγμα αυτού του περιεχομένου με προσωπικές εφαρμογές"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Το προφίλ εργασίας σας έχει τεθεί σε παύση."</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Πατήστε για ενεργοποίηση"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Οι εφαρμογές εργασιών τέθηκαν σε παύση"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Αναίρεση παύσης"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Δεν υπάρχουν εφαρμογές εργασιών"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Δεν υπάρχουν προσωπικές εφαρμογές"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Θέλετε να ανοίξετε την εφαρμογή <xliff:g id="APP">%s</xliff:g> στο προσωπικό σας προφίλ;"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Συμπερίληψη κειμένου"</string>      <string name="exclude_link" msgid="1332778255031992228">"Εξαίρεση συνδέσμου"</string>      <string name="include_link" msgid="827855767220339802">"Συμπερίληψη συνδέσμου"</string> +    <string name="pinned" msgid="7623664001331394139">"Καρφιτσωμένο"</string>  </resources> diff --git a/java/res/values-en-rAU/strings.xml b/java/res/values-en-rAU/strings.xml index 29707f24..a1438ed9 100644 --- a/java/res/values-en-rAU/strings.xml +++ b/java/res/values-en-rAU/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>      <string name="include_link" msgid="827855767220339802">"Include link"</string> +    <string name="pinned" msgid="7623664001331394139">"Pinned"</string>  </resources> diff --git a/java/res/values-en-rCA/strings.xml b/java/res/values-en-rCA/strings.xml index 29707f24..a1438ed9 100644 --- a/java/res/values-en-rCA/strings.xml +++ b/java/res/values-en-rCA/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>      <string name="include_link" msgid="827855767220339802">"Include link"</string> +    <string name="pinned" msgid="7623664001331394139">"Pinned"</string>  </resources> diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml index 29707f24..a1438ed9 100644 --- a/java/res/values-en-rGB/strings.xml +++ b/java/res/values-en-rGB/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>      <string name="include_link" msgid="827855767220339802">"Include link"</string> +    <string name="pinned" msgid="7623664001331394139">"Pinned"</string>  </resources> diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml index 29707f24..a1438ed9 100644 --- a/java/res/values-en-rIN/strings.xml +++ b/java/res/values-en-rIN/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>      <string name="include_link" msgid="827855767220339802">"Include link"</string> +    <string name="pinned" msgid="7623664001331394139">"Pinned"</string>  </resources> diff --git a/java/res/values-en-rXC/strings.xml b/java/res/values-en-rXC/strings.xml index 5811516b..56574b6c 100644 --- a/java/res/values-en-rXC/strings.xml +++ b/java/res/values-en-rXC/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>      <string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>      <string name="include_link" msgid="827855767220339802">"Include link"</string> +    <string name="pinned" msgid="7623664001331394139">"Pinned"</string>  </resources> diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml index 5393889e..97ae9a6c 100644 --- a/java/res/values-es-rUS/strings.xml +++ b/java/res/values-es-rUS/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} y # archivo más}many{{file_name} y # archivos más}other{{file_name} y # archivos más}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # archivo}many{+ # de archivos}other{+ # archivos}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Compartiendo texto"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Compartiendo vínculo"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartiendo imagen}many{Compartiendo # de imág.}other{Compartiendo # imágenes}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# archivo más}many{# de archivos más}other{# archivos más}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Compartir texto"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Compartir vínculo"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartir la imagen}many{Compartir # de imágenes}other{Compartir # imágenes}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartiendo video}many{Compartiendo # de videos}other{Compartiendo # videos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartiendo # elemento}many{Compartiendo # de elem.}other{Compartiendo # elementos}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartiendo con texto"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartiendo con vínculo"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Se compartirá # archivo}many{Se compartirán # de archivos}other{Se compartirán # archivos}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartir imagen con texto}many{Compartir # de imágenes con texto}other{Compartir # imágenes con texto}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartir imagen con vínculo}many{Compartir # de imágenes con vínculo}other{Compartir # imágenes con vínculo}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartir video con texto}many{Compartir # de videos con texto}other{Compartir # videos con texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de vista previa de la imagen"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de vista previa del video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de vista previa del archivo"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hay personas recomendadas con quienes compartir"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No se puede abrir este contenido con apps de trabajo"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"No se pueden usar apps personales para compartir este contenido"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"No se puede abrir este contenido con apps personales"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de trabajo está en pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Presionar para activar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Se pausaron las apps de trabajo"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reanudar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"El contenido no es compatible con apps de trabajo"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"El contenido no es compatible con apps personales"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Quieres abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Incluir texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluir vínculo"</string>      <string name="include_link" msgid="827855767220339802">"Incluir vínculo"</string> +    <string name="pinned" msgid="7623664001331394139">"Fijado"</string>  </resources> diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml index 5be4c35a..0c42bb82 100644 --- a/java/res/values-es/strings.xml +++ b/java/res/values-es/strings.xml @@ -51,19 +51,28 @@      <string name="activity_resolver_use_once" msgid="594173435998892989">"Solo una vez"</string>      <string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"<xliff:g id="APP">%1$s</xliff:g> no admite perfiles de trabajo"</string>      <string name="pin_specific_target" msgid="5057063421361441406">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> -    <string name="unpin_specific_target" msgid="3115158908159857777">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> +    <string name="unpin_specific_target" msgid="3115158908159857777">"Desfijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} y # archivo más}many{{file_name} y # archivos más}other{{file_name} y # archivos más}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # archivo}many{+ # archivos}other{+ # archivos}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Compartiendo vídeo}many{Compartiendo # vídeos}other{Compartiendo # vídeos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartiendo # elemento}many{Compartiendo # elementos}other{Compartiendo # elementos}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartiendo imagen con texto"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartiendo imagen con enlace"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartiendo imagen con enlace}many{Compartiendo # imágenes con enlace}other{Compartiendo # imágenes con enlace}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartiendo vídeo con texto}many{Compartiendo # vídeos con texto}other{Compartiendo # vídeos con texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de previsualización de la imagen"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de previsualización del vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de previsualización del archivo"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hay sugerencias de personas con las que compartir"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicaciones"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contenido no se puede abrir con aplicaciones de trabajo"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Este contenido no se puede compartir con aplicaciones personales"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Este contenido no se puede abrir con aplicaciones personales"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de trabajo está en pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toca para activar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Las aplicaciones de trabajo están en pausa"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ninguna aplicación de trabajo"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ninguna aplicación personal"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string> @@ -86,4 +95,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>  </resources> diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml index 60ba3db6..bc960699 100644 --- a/java/res/values-et/strings.xml +++ b/java/res/values-et/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Kinnita <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Vabasta <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Muuda"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fail}other{{file_name} + # faili}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fail}other{+ # faili}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Veel # fail}other{Veel # faili}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Teksti jagamine"</string>      <string name="sharing_link" msgid="2307694372813942916">"Lingi jagamine"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Pildi jagamine}other{# pildi jagamine}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video jagamine}other{# video jagamine}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# üksuse jagamine}other{# üksuse jagamine}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Pildi jagamine tekstiga"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Pildi jagamine lingiga"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# faili jagamine}other{# faili jagamine}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Teksti sisaldava pildi jagamine}other{# teksti sisaldava pildi jagamine}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Linki sisaldava pildi jagamine}other{# linki sisaldava pildi jagamine}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Teksti sisaldava video jagamine}other{# teksti sisaldava video jagamine}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Pildi eelvaate pisipilt"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video eelvaate pisipilt"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faili eelvaate pisipilt"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ei ole ühtki soovitatud inimest, kellega jagada"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Rakenduste loend"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Seda sisu ei saa isiklike rakendustega jagada"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Seda sisu ei saa isiklike rakendustega jagada."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Seda sisu ei saa isiklike rakendustega avada"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Tööprofiil on peatatud"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Puudutage sisselülitamiseks"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Töörakendused on peatatud"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Jätka"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Töörakendusi pole"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Isiklikke rakendusi pole"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Kas avada <xliff:g id="APP">%s</xliff:g> teie isiklikul profiilil?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Kaasa tekst"</string>      <string name="exclude_link" msgid="1332778255031992228">"Välista link"</string>      <string name="include_link" msgid="827855767220339802">"Kaasa link"</string> +    <string name="pinned" msgid="7623664001331394139">"Kinnitatud"</string>  </resources> diff --git a/java/res/values-eu/strings.xml b/java/res/values-eu/strings.xml index 1a613e7f..1cc7576b 100644 --- a/java/res/values-eu/strings.xml +++ b/java/res/values-eu/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Ainguratu <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Kendu aingura <xliff:g id="LABEL">%1$s</xliff:g> aplikazioari"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editatu"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} eta beste # fitxategi}other{{file_name} eta beste # fitxategi}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{eta beste # fitxategi}other{eta beste # fitxategi}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Testua partekatzen"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{eta beste # fitxategi}other{eta beste # fitxategi}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Partekatuko den testua"</string>      <string name="sharing_link" msgid="2307694372813942916">"Esteka partekatzen"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Irudia partekatzen}other{# irudi partekatzen}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Bideoa partekatzen}other{# bideo partekatzen}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# elementu partekatzen}other{# elementu partekatzen}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Irudi testuduna partekatzen"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Irudi estekaduna partekatzen"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fitxategi partekatuko da}other{# fitxategi partekatuko dira}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Irudi testudun bat partekatuko da}other{# irudi testudun partekatuko dira}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Irudi estekadun bat partekatuko da}other{# irudi estekadun partekatuko dira}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Bideo testudun bat partekatuko da}other{# bideo testudun partekatuko dira}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Irudiaren aurrebista gisako irudi txikia"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Bideoaren aurrebista gisako irudi txikia"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fitxategiaren aurrebista gisako irudi txikia"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ez dago edukia partekatzeko pertsona gomendaturik"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Aplikazioen zerrenda"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Eduki hau ezin da laneko aplikazioekin ireki"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Eduki hau ezin da aplikazio pertsonalekin partekatu"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Eduki hau ezin da aplikazio pertsonalekin ireki"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Laneko profila pausatuta dago"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Sakatu aktibatzeko"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pausatuta daude laneko aplikazioak"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Berraktibatu"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ez dago laneko aplikaziorik"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ez dago aplikazio pertsonalik"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Profil pertsonalean ireki nahi duzu <xliff:g id="APP">%s</xliff:g>?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Sartu testua"</string>      <string name="exclude_link" msgid="1332778255031992228">"Utzi kanpoan esteka"</string>      <string name="include_link" msgid="827855767220339802">"Sartu esteka"</string> +    <string name="pinned" msgid="7623664001331394139">"Ainguratuta"</string>  </resources> diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml index bb4a1a69..58313f70 100644 --- a/java/res/values-fa/strings.xml +++ b/java/res/values-fa/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> @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"سنجاق کردن <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"برداشتن سنجاق <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ویرایش"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # فایل}one{{file_name} + # فایل}other{{file_name} + # فایل}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{بیشاز # فایل}one{بیشاز # فایل}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{درحال همرسانی # تصویر}other{درحال همرسانی # تصویر}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{بیشاز # فایل دیگر}one{بیشاز # فایل دیگر}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{همرسانی # تصویر}other{همرسانی # تصویر}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{درحال همرسانی ویدیو}one{درحال همرسانی # ویدیو}other{درحال همرسانی # ویدیو}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{درحال همرسانی # مورد}one{درحال همرسانی # مورد}other{درحال همرسانی # مورد}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"همرسانی تصویر با نوشتار"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"همرسانی تصویر با پیوند"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{همرسانی # فایل}one{همرسانی # فایل}other{همرسانی # فایل}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{درحال همرسانی تصویر با نوشتار}one{درحال همرسانی # تصویر با نوشتار}other{درحال همرسانی # تصویر با نوشتار}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{درحال همرسانی تصویر با پیوند}one{درحال همرسانی # تصویر با پیوند}other{درحال همرسانی # تصویر با پیوند}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{درحال همرسانی ویدیو با نوشتار}one{درحال همرسانی # ویدیو با نوشتار}other{درحال همرسانی # ویدیو با نوشتار}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"تصویر کوچک پیشنمای تصویر"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"تصویر کوچک پیشنمای ویدیو"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"تصویر کوچک پیشنمای فایل"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"هیچ فردی توصیه نشده است که با او همرسانی کنید"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"فهرست برنامهها"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"نمیتوان این محتوا را با برنامههای کاری باز کرد"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"نمیتوان این محتوا را با برنامههای شخصی همرسانی کرد"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"نمیتوان این محتوا را با برنامههای شخصی باز کرد"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"نمایه کاری موقتاً متوقف شده است"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"برای روشن کردن، ضربه بزنید"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"برنامههای کاری موقتاً متوقف شدهاند"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"لغو مکث"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"برنامه کاریای وجود ندارد"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"برنامه شخصیای وجود ندارد"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> در نمایه شخصی باز شود؟"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"لحاظ کردن نوشتار"</string>      <string name="exclude_link" msgid="1332778255031992228">"مستثنی کردن پیوند"</string>      <string name="include_link" msgid="827855767220339802">"لحاظ کردن پیوند"</string> +    <string name="pinned" msgid="7623664001331394139">"سنجاقشده"</string>  </resources> diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml index 3c60b384..53537e67 100644 --- a/java/res/values-fi/strings.xml +++ b/java/res/values-fi/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Kiinnitä <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Irrota <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Muokkaa"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # tiedosto}other{{file_name} + # tiedostoa}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{yli # tiedosto}other{yli # tiedostoa}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # muu tiedosto}other{+ # muuta tiedostoa}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Jaetaan tekstiä"</string>      <string name="sharing_link" msgid="2307694372813942916">"Jaetaan linkkiä"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Jaetaan kuvaa}other{Jaetaan # kuvaa}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Jaetaan videota}other{Jaetaan # videota}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Jaetaan # kohdetta}other{Jaetaan # kohdetta}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Kuvaa ja tekstiä jaetaan"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Kuvaa ja linkkiä jaetaan"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Jaetaan # tiedosto}other{Jaetaan # tiedostoa}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Kuvaa ja tekstiä jaetaan}other{# kuvaa ja tekstiä jaetaan}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Kuvaa ja linkkiä jaetaan}other{# kuvaa ja linkkiä jaetaan}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Videota ja tekstiä jaetaan}other{# videota ja tekstiä jaetaan}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Kuvan esikatselun pikkukuva"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Videon esikatselun pikkukuva"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Tiedoston esikatselun pikkukuva"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ei suosituksia kenelle jakaa"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Sovellusluettelo"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tätä sisältöä ei voi avata työsovelluksilla"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tätä sisältöä ei voi jakaa henkilökohtaisilla sovelluksilla"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tätä sisältöä ei voi avata henkilökohtaisilla sovelluksilla"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Työprofiilin käyttö on keskeytetty"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Laita päälle napauttamalla"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Työsovellukset on keskeytetty"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Jatka käyttöä"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ei työsovelluksia"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ei henkilökohtaisia sovelluksia"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Avataanko <xliff:g id="APP">%s</xliff:g> henkilökohtaisessa profiilissa?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Liitä teksti mukaan"</string>      <string name="exclude_link" msgid="1332778255031992228">"Jätä linkki pois"</string>      <string name="include_link" msgid="827855767220339802">"Liitä linkki mukaan"</string> +    <string name="pinned" msgid="7623664001331394139">"Kiinnitetty"</string>  </resources> diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml index 47bea8ac..5595b6cc 100644 --- a/java/res/values-fr-rCA/strings.xml +++ b/java/res/values-fr-rCA/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Épingler <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Annuler l\'épinglage de <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Modifier"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fichier}one{{file_name} + # fichier}many{{file_name} + # fichiers}other{{file_name} + # fichiers}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fichier}one{+ # fichier}many{+ # de fichiers}other{+ # fichiers}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Partage du message texte…"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Partage du lien en cours…"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage de l\'image…}one{Partage de # image…}many{Partage de # d\'images…}other{Partage de # images…}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{et # fichier supplémentaire}one{et # fichier supplémentaire}many{et # de fichiers supplémentaires}other{et # fichiers supplémentaires}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Partage de texte"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Partage d\'un lien"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage d\'une image}one{Partage de # image}many{Partage de # d\'images}other{Partage de # images}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Partage de la vidéo…}one{Partage de # vidéo…}many{Partage de # de vidéos…}other{Partage de # vidéos…}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Partage de # élément…}one{Partage de # élément…}many{Partage de # d\'éléments}other{Partage de # éléments…}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Partage d\'image avec texte…"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Partage d\'image avec lien…"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Partage de # fichier en cours…}one{Partage de # fichier en cours…}many{Partage de # de fichiers en cours…}other{Partage de # fichiers en cours…}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Partage d\'une image avec du texte}one{Partage de # image avec du texte}many{Partage de # d\'images avec du texte}other{Partage de # images avec du texte}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Partage d\'une image avec un lien}one{Partage de # image avec un lien}many{Partage de # d\'images avec un lien}other{Partage de # images avec un lien}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Partage d\'une vidéo avec du texte}one{Partage de # vidéo avec du texte}many{Partage de # de vidéos avec du texte}other{Partage de # vidéos avec du texte}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniature d\'aperçu de l\'image"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniature d\'aperçu de la vidéo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniature d\'aperçu du fichier"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucune recommandation de personnes avec lesquelles effectuer un partage"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste des applications"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applications professionnelles"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Impossible de partager ce contenu avec des applications personnelles"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Impossible d\'ouvrir ce contenu avec des applications personnelles"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Le profil professionnel est interrompu"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Touchez pour activer"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les applications professionnelles sont interrompues"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Réactiver"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Aucune application professionnelle"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Aucune application personnelle"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inclure le texte"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclure le lien"</string>      <string name="include_link" msgid="827855767220339802">"Inclure le lien"</string> +    <string name="pinned" msgid="7623664001331394139">"Épinglée"</string>  </resources> diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml index fbdb3a14..5f0c85e0 100644 --- a/java/res/values-fr/strings.xml +++ b/java/res/values-fr/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Épingler <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Retirer <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Modifier"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fichier}one{{file_name} + # fichier}many{{file_name} + # fichiers}other{{file_name} + # fichiers}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fichier}one{+ # fichier}many{+ # fichiers}other{+ # fichiers}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # autre fichier}one{+ # autre fichier}many{+ # autres fichiers}other{+ # autres fichiers}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Partage du texte…"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Partage du lien…"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Partager le lien"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage de l\'image…}one{Partage de # image…}many{Partage de # d\'images…}other{Partage de # images…}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Partage de la vidéo…}one{Partage de # vidéo…}many{Partage de # de vidéos…}other{Partage de # vidéos…}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Partage de # élément…}one{Partage de # élément…}many{Partage de # d\'éléments…}other{Partage de # éléments…}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Partage de l\'image (texte)"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Partage de l\'image (lien)"</string> -    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucune recommandation de personnes avec lesquelles effectuer un partage"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste des applications"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Partage de # fichier}one{Partage de # fichier}many{Partage de # fichiers}other{Partage de # fichiers}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Partager 1 image avec du texte}one{Partager # image avec du texte}many{Partager # images avec du texte}other{Partager # images avec du texte}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Partager 1 image avec un lien}one{Partager # image avec un lien}many{Partager # images avec un lien}other{Partager # images avec un lien}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Partager 1 vidéo avec du texte}one{Partager # vidéo avec du texte}many{Partager # vidéos avec du texte}other{Partager # vidéos avec du texte}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Vignette d\'aperçu de l\'image"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Vignette d\'aperçu de la vidéo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Vignette d\'aperçu du fichier"</string> +    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucun destinataire recommandé"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applis professionnelles"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Impossible de partager ce contenu avec des applis personnelles"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Impossible d\'ouvrir ce contenu avec des applis personnelles"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil professionnel en pause"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Appuyez pour l\'activer"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les applis professionnelles sont en pause"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Réactiver"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Aucune appli professionnelle"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Aucune appli personnelle"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inclure le texte"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclure le lien"</string>      <string name="include_link" msgid="827855767220339802">"Inclure le lien"</string> +    <string name="pinned" msgid="7623664001331394139">"Épinglée"</string>  </resources> diff --git a/java/res/values-gl/strings.xml b/java/res/values-gl/strings.xml index f50f61b8..60dc78de 100644 --- a/java/res/values-gl/strings.xml +++ b/java/res/values-gl/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Deixar de fixar a <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ficheiro}other{{file_name} + # ficheiros}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+# ficheiro}other{+# ficheiros}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# ficheiro máis}other{# ficheiros máis}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Compartindo texto"</string>      <string name="sharing_link" msgid="2307694372813942916">"Compartindo ligazón"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartindo imaxe}other{Compartindo # imaxes}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartindo vídeo}other{Compartindo # vídeos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartindo # elemento}other{Compartindo # elementos}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartindo imaxe (texto)"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartindo imaxe (lig.)"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartindo # ficheiro}other{Compartindo # ficheiros}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartindo imaxe con texto}other{Compartindo # imaxes con texto}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartindo imaxe con ligazón}other{Compartindo # imaxes con ligazón}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartindo vídeo con texto}other{Compartindo # vídeos con texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de vista previa da imaxe"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de vista previa do vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de vista previa do ficheiro"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Non hai recomendacións de persoas coas que compartir contido"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicacións"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contido non pode abrirse con aplicacións do traballo"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Este contido non pode compartirse con aplicacións persoais"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Este contido non pode abrirse con aplicacións persoais"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de traballo está en pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocar para activar o perfil"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"As aplicacións do traballo están en pausa"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Non hai ningunha aplicación do traballo compatible"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Non hai ningunha aplicación persoal compatible"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Queres abrir <xliff:g id="APP">%s</xliff:g> no teu perfil persoal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Incluír texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluír ligazón"</string>      <string name="include_link" msgid="827855767220339802">"Incluír ligazón"</string> +    <string name="pinned" msgid="7623664001331394139">"Elemento fixado"</string>  </resources> diff --git a/java/res/values-gu/strings.xml b/java/res/values-gu/strings.xml index b9d846e2..db3bd59a 100644 --- a/java/res/values-gu/strings.xml +++ b/java/res/values-gu/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>ને પિન કરો"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ને અનપિન કરો"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ફેરફાર કરો"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ફાઇલ}one{{file_name} + # ફાઇલ}other{{file_name} + # ફાઇલો}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ફાઇલ}one{+ # ફાઇલ}other{+ # ફાઇલ}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ વધુ # ફાઇલ}one{+ વધુ # ફાઇલ}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{# છબી શેર કરી રહ્યાં છીએ}other{# છબી શેર કરી રહ્યાં છીએ}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{વીડિયો શેર કરીએ છીએ}one{# વીડિયો શેર કરીએ છીએ}other{# વીડિયો શેર કરીએ છીએ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# આઇટમ શેર કરી રહ્યાં છીએ}one{# આઇટમ શેર કરી રહ્યાં છીએ}other{# આઇટમ શેર કરી રહ્યાં છીએ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ટેક્સ્ટ સાથે છબી શેર થશે"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"લિંક સાથે છબી શેર થાય છે"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ફાઇલ શેર કરી રહ્યાં છીએ}one{# ફાઇલ શેર કરી રહ્યાં છીએ}other{# ફાઇલ શેર કરી રહ્યાં છીએ}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ટેક્સ્ટ સાથે છબી શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # છબી શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # છબી શેર કરી રહ્યાં છીએ}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{લિંક સાથે છબી શેર કરી રહ્યાં છીએ}one{લિંક સાથે # છબી શેર કરી રહ્યાં છીએ}other{લિંક સાથે # છબી શેર કરી રહ્યાં છીએ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ટેક્સ્ટ સાથે વીડિયો શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # વીડિયો શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # વીડિયો શેર કરી રહ્યાં છીએ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"છબીના પ્રીવ્યૂની થંબનેલ"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"વીડિયોના પ્રીવ્યૂની થંબનેલ"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ફાઇલના પ્રીવ્યૂની થંબનેલ"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"શેર કરવા માટે સુઝાવ આપવામાં આવેલા કોઈ લોકો નથી"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ઍપની સૂચિ"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"આ કન્ટેન્ટ ઑફિસ માટેની ઍપ વડે ખોલી શકાતું નથી"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"આ કન્ટેન્ટ વ્યક્તિગત ઍપ સાથે શેર કરી શકાતું નથી"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"આ કન્ટેન્ટ વ્યક્તિગત ઍપ વડે ખોલી શકાતું નથી"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ઑફિસની પ્રોફાઇલ થોભાવી છે"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ચાલુ કરવા માટે ટૅપ કરો"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ઑફિસ માટેની ઍપ થોભાવવામાં આવી છે"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ફરી ચાલુ કરો"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"કોઈ ઑફિસ માટેની ઍપ સપોર્ટ કરતી નથી"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"કોઈ વ્યક્તિગત ઍપ સપોર્ટ કરતી નથી"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"તમારી વ્યક્તિગત પ્રોફાઇલમાં <xliff:g id="APP">%s</xliff:g> ખોલીએ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ટેક્સ્ટ શામેલ કરો"</string>      <string name="exclude_link" msgid="1332778255031992228">"લિંકને બાકાત કરો"</string>      <string name="include_link" msgid="827855767220339802">"લિંક શામેલ કરો"</string> +    <string name="pinned" msgid="7623664001331394139">"પિન કરેલી"</string>  </resources> diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml index 538b11dd..b722e0ce 100644 --- a/java/res/values-hi/strings.xml +++ b/java/res/values-hi/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> को पिन करें"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> को अनपिन करें"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"बदलाव करें"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फ़ाइल}one{{file_name} + # फ़ाइल}other{{file_name} + # फ़ाइलें}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # फ़ाइल}one{+ # फ़ाइल}other{+ # फ़ाइलें}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# और फ़ाइल}one{# और फ़ाइल}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{# इमेज शेयर की जा रही है}other{# इमेज शेयर की जा रही हैं}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{वीडियो शेयर किया जा रहा है}one{# वीडियो शेयर किया जा रहा है}other{# वीडियो शेयर किए जा रहे हैं}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# आइटम शेयर किया जा रहा है}one{# आइटम शेयर किया जा रहा है}other{# आइटम शेयर किए जा रहे हैं}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"टेक्स्ट के साथ इमेज शेयर की जा रही है"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंक के साथ इमेज शेयर की जा रही है"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# फ़ाइल शेयर की जा रही है}one{# फ़ाइल शेयर की जा रही है}other{# फ़ाइलें शेयर की जा रही हैं}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{टेक्स्ट के साथ इमेज शेयर की जा रही है}one{टेक्स्ट के साथ # इमेज शेयर की जा रही है}other{टेक्स्ट के साथ # इमेज शेयर की जा रही हैं}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंक के साथ इमेज शेयर की जा रही है}one{लिंक के साथ # इमेज शेयर की जा रही है}other{लिंक के साथ # इमेज शेयर की जा रही हैं}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{टेक्स्ट के साथ वीडियो शेयर किया जा रहा है}one{टेक्स्ट के साथ # वीडियो शेयर किया जा रहा है}other{टेक्स्ट के साथ # वीडियो शेयर किए जा रहे हैं}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"इमेज के थंबनेल की झलक"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"वीडियो के थंबनेल की झलक"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"फ़ाइल के थंबनेल की झलक"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"शेयर करने के लिए, किसी व्यक्ति का सुझाव नहीं दिया गया है"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ऐप्लिकेशन की सूची"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"इस कॉन्टेंट को निजी ऐप्लिकेशन का इस्तेमाल करके, शेयर नहीं किया जा सकता"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"इस कॉन्टेंट को निजी ऐप्लिकेशन के ज़रिए शेयर नहीं किया जा सकता"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"इस कॉन्टेंट को निजी ऐप्लिकेशन पर खोला नहीं जा सकता"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"वर्क प्रोफ़ाइल रोक दी गई है"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"वर्क प्रोफ़ाइल चालू करने के लिए टैप करें"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"वर्क ऐप्लिकेशन बंद किए गए हैं"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"चालू करें"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"यह कॉन्टेंट, ऑफ़िस के काम से जुड़े आपके किसी भी ऐप्लिकेशन पर खोला नहीं जा सकता"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"यह कॉन्टेंट आपके किसी भी निजी ऐप्लिकेशन पर खोला नहीं जा सकता"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"क्या <xliff:g id="APP">%s</xliff:g> को निजी प्रोफ़ाइल में खोलना है?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"टेक्स्ट जोड़ें"</string>      <string name="exclude_link" msgid="1332778255031992228">"लिंक हटाएं"</string>      <string name="include_link" msgid="827855767220339802">"लिंक जोड़ें"</string> +    <string name="pinned" msgid="7623664001331394139">"पिन किया गया"</string>  </resources> diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml index ef08630e..e2d71b37 100644 --- a/java/res/values-hr/strings.xml +++ b/java/res/values-hr/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Prikvači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Otkvači sudionika <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # datoteka}one{{file_name} i # datoteka}few{{file_name} i # datoteke}other{{file_name} i # datoteka}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # datoteka}one{+ # datoteka}few{+ # datoteke}other{+ # datoteka}}"</string> +    <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_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_items" msgid="5266543892527310331">"{count,plural, =1{Dijeli se # stavka}one{Dijeli se # stavka}few{Dijele se # stavke}other{Dijeli se # stavki}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Dijeli se slika s tekstom"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Dijeli se slika s vezom"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Dijeli se slika s vezom}one{Dijeli se # slika s vezom}few{Dijele se # slike s vezom}other{Dijeli se # slika s vezom}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Dijeli se videozapis s tekstom}one{Dijeli se # videozapis s tekstom}few{Dijele se # videozapisa s tekstom}other{Dijeli se # videozapisa s tekstom}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Minijatura pregleda slike"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Minijatura pregleda videozapisa"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Minijatura pregleda datoteke"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih osoba za dijeljenje"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Popis aplikacija"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Taj se sadržaj ne može otvoriti pomoću poslovnih aplikacija"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Taj se sadržaj ne može dijeliti pomoću osobnih aplikacija"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Taj se sadržaj ne može otvoriti pomoću osobnih aplikacija"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Poslovni profil je pauziran"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da biste uključili"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovno pokreni"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Poslovne aplikacije nisu dostupne"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Osobne aplikacije nisu dostupne"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite li otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na osobnom profilu?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Uključi tekst"</string>      <string name="exclude_link" msgid="1332778255031992228">"Isključi vezu"</string>      <string name="include_link" msgid="827855767220339802">"Uključi vezu"</string> +    <string name="pinned" msgid="7623664001331394139">"Prikvačeno"</string>  </resources> diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml index 15b79c6d..53ddba7f 100644 --- a/java/res/values-hu/strings.xml +++ b/java/res/values-hu/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> kitűzése"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> rögzítésének feloldása"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Szerkesztés"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fájl}other{{file_name} + # fájl}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fájl}other{+ # fájl}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{További # fájl}other{További # fájl}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Szöveg megosztása"</string>      <string name="sharing_link" msgid="2307694372813942916">"Link megosztása"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Kép megosztása}other{# kép megosztása}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Videó megosztása}other{# videó megosztása}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# elem megosztása}other{# elem megosztása}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Kép megosztása szöveggel…"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Kép megosztása linkkel…"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fájl megosztása}other{# fájl megosztása}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Kép megosztása szöveggel}other{# kép megosztása szöveggel}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Kép megosztása linkkel}other{# kép megosztása linkkel}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Videó megosztása szöveggel}other{# videó megosztása szöveggel}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Kép előnézeti indexképe"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Videó előnézeti indexképe"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fájl előnézeti indexképe"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nincsenek ajánlott személyek a megosztáshoz"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Alkalmazások listája"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ez a tartalom nem nyitható meg munkahelyi alkalmazásokkal"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ez a tartalom nem osztható meg személyes alkalmazásokkal"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ez a tartalom nem nyitható meg személyes alkalmazásokkal"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"A munkaprofil használata szünetel"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Koppintson a bekapcsoláshoz"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"A munkahelyi alkalmazások szüneteltetve vannak"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Szüneteltetés feloldása"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nincs munkahelyi alkalmazás"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nincs személyes alkalmazás"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Megnyitja a(z) <xliff:g id="APP">%s</xliff:g> alkalmazást a személyes profil használatával?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Szöveggel együtt"</string>      <string name="exclude_link" msgid="1332778255031992228">"Link eltávolítása"</string>      <string name="include_link" msgid="827855767220339802">"Linkkel együtt"</string> +    <string name="pinned" msgid="7623664001331394139">"Kitűzve"</string>  </resources> diff --git a/java/res/values-hy/strings.xml b/java/res/values-hy/strings.xml index 64f1b7f6..6a83cdaa 100644 --- a/java/res/values-hy/strings.xml +++ b/java/res/values-hy/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Ամրացնել <xliff:g id="LABEL">%1$s</xliff:g> հավելվածը"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Ապամրացնել <xliff:g id="LABEL">%1$s</xliff:g> հավելվածը"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Փոփոխել"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ու ևս # ֆայլ}one{{file_name} ու ևս # ֆայլ}other{{file_name} ու ևս # ֆայլ}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ֆայլ}one{# ֆայլ}other{# ֆայլ}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Ու ևս # ֆայլ}one{Ու ևս # ֆայլ}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{# պատկերի ուղարկում}other{# պատկերի ուղարկում}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Տեսանյութի ուղարկում}one{# տեսանյութի ուղարկում}other{# տեսանյութի ուղարկում}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# տարրի ուղարկում}one{# տարրի ուղարկում}other{# տարրի ուղարկում}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Պատկերի+տեքստի ուղարկում"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Պատկերի և հղման ուղարկում"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Ուղարկվում է # ֆայլ}one{Ուղարկվում է # ֆայլ}other{Ուղարկվում է # ֆայլ}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}one{# պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}other{# պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Պատկերի ուղարկում հղման միջոցով}one{# պատկերի ուղարկում հղման միջոցով}other{# պատկերի ուղարկում հղման միջոցով}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}one{# տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}other{# տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Պատկերի նախադիտման մանրապատկեր"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Տեսանյութի նախադիտման մանրապատկեր"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Ֆայլի նախադիտման մանրապատկեր"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Չկան օգտատերեր, որոնց հետ կարող եք կիսվել"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Հավելվածների ցանկ"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Այս բովանդակությունը հնարավոր չէ բացել աշխատանքային հավելվածներով"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Այս բովանդակությունը հնարավոր չէ ուղարկել անձնական հավելվածներով"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Այս բովանդակությունը հնարավոր չէ բացել անձնական հավելվածներով"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Աշխատանքային պրոֆիլի ծառայությունը դադարեցված է"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Հպեք միացնելու համար"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Աշխատանքային հավելվածները դադարեցված են"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Նորից միացնել"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Աշխատանքային հավելվածներ չկան"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Անձնական հավելվածներ չկան"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Բացե՞լ <xliff:g id="APP">%s</xliff:g> հավելվածը ձեր անձնական պրոֆիլում"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Ներառել տեքստը"</string>      <string name="exclude_link" msgid="1332778255031992228">"Բացառել հղումը"</string>      <string name="include_link" msgid="827855767220339802">"Ներառել հղումը"</string> +    <string name="pinned" msgid="7623664001331394139">"Ամրացված է"</string>  </resources> diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml index 5c5ba638..d7400b80 100644 --- a/java/res/values-in/strings.xml +++ b/java/res/values-in/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Sematkan <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Lepas sematan <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # file}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # file}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Membagikan teks"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Membagikan link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Membagikan gambar}other{Membagikan # gambar}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # file lainnya}other{+ # file lainnya}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Berbagi teks"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Berbagi link"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Berbagi gambar}other{Berbagi # gambar}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Membagikan video}other{Membagikan # video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Membagikan # item}other{Membagikan # item}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Membagikan gambar dengan teks"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Membagikan gambar dengan link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Membagikan # file}other{Membagikan # file}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Membagikan gambar dengan teks}other{Membagikan # gambar dengan teks}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Membagikan gambar dengan link}other{Membagikan # gambar dengan link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Membagikan video dengan teks}other{Membagikan # video dengan teks}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Thumbnail pratinjau gambar"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Thumbnail pratinjau video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Thumbnail pratinjau file"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tidak ada rekomendasi kontak untuk berbagi"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Daftar aplikasi"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Konten ini tidak dapat dibagikan dengan aplikasi pribadi"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Konten ini tidak dapat dibagikan ke aplikasi pribadi"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Konten ini tidak dapat dibuka dengan aplikasi pribadi"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil kerja dijeda"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ketuk untuk mengaktifkan"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikasi kerja dijeda"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Batalkan jeda"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Tidak ada aplikasi kerja"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Tidak ada aplikasi pribadi"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buka <xliff:g id="APP">%s</xliff:g> di profil pribadi?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Sertakan teks"</string>      <string name="exclude_link" msgid="1332778255031992228">"Kecualikan link"</string>      <string name="include_link" msgid="827855767220339802">"Sertakan link"</string> +    <string name="pinned" msgid="7623664001331394139">"Disematkan"</string>  </resources> diff --git a/java/res/values-is/strings.xml b/java/res/values-is/strings.xml index 04a99b79..8e0a9f4f 100644 --- a/java/res/values-is/strings.xml +++ b/java/res/values-is/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Festa <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Losa <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Breyta"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # skrá}one{{file_name} + # skrá}other{{file_name} + # skrár}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # skrá}one{+ # skrá}other{+ # skrár}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # skrá í viðbót}one{+ # skrá í viðbót}other{+ # skrár í viðbót}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deilir texta"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deilir tengli"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deilir mynd}one{Deilir # mynd}other{Deilir # myndum}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deilir myndskeiði}one{Deilir # myndskeiði}other{Deilir # myndskeiðum}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deilir # atriði}one{Deilir # atriði}other{Deilir # atriðum}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deilir mynd með texta"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deilir mynd með tengli"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deilir # skrá}one{Deilir # skrá}other{Deilir # skrám}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deilir mynd með texta}one{Deilir # mynd með texta}other{Deilir # myndum með texta}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deilir mynd með tengli}one{Deilir # mynd með tengli}other{Deilir # myndum með tengli}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deilir myndskeiði með texta}one{Deilir # myndskeiði með texta}other{Deilir # myndskeiðum með texta}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Forskoðunarsmámynd myndar"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Forskoðunarsmámynd myndskeiðs"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Forskoðunarsmámynd skráar"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Engar tillögur um fólk til að deila með"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Forritalisti"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ekki er hægt að opna þetta efni með vinnuforritum"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ekki er hægt að deila þessu efni með forritum til einkanota"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ekki er hægt að opna þetta efni með forritum til einkanota"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Hlé gert á vinnusniði"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ýttu til að kveikja"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Hlé gert á vinnuforritum"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ljúka hléi"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Engin vinnuforrit"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Engin forrit til einkanota"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Opna <xliff:g id="APP">%s</xliff:g> í þínu eigin sniði?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Hafa texta með"</string>      <string name="exclude_link" msgid="1332778255031992228">"Útiloka tengil"</string>      <string name="include_link" msgid="827855767220339802">"Hafa tengil með"</string> +    <string name="pinned" msgid="7623664001331394139">"Fest"</string>  </resources> diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml index dc23c628..38aba0c2 100644 --- a/java/res/values-it/strings.xml +++ b/java/res/values-it/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fissa <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Sblocca <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Modifica"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}many{{file_name} + # file}other{{file_name} + # file}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}many{+ # file}other{+ # file}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Condivisione del testo…"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Condivisione del link…"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Condivisione immagine…}many{Condivis. di # immagini…}other{Condivis. di # immagini…}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # altro file}many{+ altri # file}other{+ altri # file}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Condivisione del testo"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Condivisione del link"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Condivisione dell\'immagine}many{Condivisione di # immagini}other{Condivisione di # immagini}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Condivisione del video…}many{Condivisione di # video…}other{Condivisione di # video…}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Condivis. di # elemento…}many{Condivis. di # elementi…}other{Condivis. di # elementi…}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Condivis. img con testo…"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Condivis. img con link…"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Condivisione di # file in corso…}many{Condivisione di # file in corso…}other{Condivisione di # file in corso…}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Condivisione immagine con testo in corso…}many{Condivisione # immagini con testo in corso…}other{Condivisione # immagini con testo in corso…}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Condivisione immagine con link}many{Condivisione # immagini con link}other{Condivisione # immagini con link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Condivisione video con messaggio in corso…}many{Condivisione # video con messaggio in corso…}other{Condivisione # video con messaggio in corso…}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura di anteprima dell\'immagine"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura di anteprima del video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura di anteprima del file"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nessuna persona consigliata per la condivisione"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Elenco di app"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Questi contenuti non possono essere aperti con app di lavoro"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Questi contenuti non possono essere condivisi con app personali"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Questi contenuti non possono essere aperti con app personali"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profilo di lavoro in pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocca per attivare"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Le app di lavoro sono in pausa"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Riattiva"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nessuna app di lavoro"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nessuna app personale"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Aprire <xliff:g id="APP">%s</xliff:g> nel tuo profilo personale?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Includi testo"</string>      <string name="exclude_link" msgid="1332778255031992228">"Escludi link"</string>      <string name="include_link" msgid="827855767220339802">"Includi link"</string> +    <string name="pinned" msgid="7623664001331394139">"Elemento fissato"</string>  </resources> diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml index 62ff1d89..c79425d8 100644 --- a/java/res/values-iw/strings.xml +++ b/java/res/values-iw/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"הצמדה של <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"ביטול ההצמדה של <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"עריכה"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ועוד קובץ אחד}one{{file_name} ועוד # קבצים}two{{file_name} ועוד # קבצים}other{{file_name} ועוד # קבצים}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ קובץ אחד}one{+ # קבצים}two{+ # קבצים}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{מתבצע שיתוף של # תמונות}two{מתבצע שיתוף של # תמונות}other{מתבצע שיתוף של # תמונות}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{קובץ אחד נוסף}one{# קבצים נוספים}two{# קבצים נוספים}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{שיתוף של # תמונות}two{שיתוף של # תמונות}other{שיתוף של # תמונות}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{מתבצע שיתוף של סרטון}one{מתבצע שיתוף של # סרטונים}two{מתבצע שיתוף של # סרטונים}other{מתבצע שיתוף של # סרטונים}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{מתבצע שיתוף של פריט אחד (#)}one{מתבצע שיתוף של # פריטים}two{מתבצע שיתוף של # פריטים}other{מתבצע שיתוף של # פריטים}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"שיתוף תמונה עם טקסט"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"שיתוף תמונה עם קישור"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{מתבצע שיתוף של קובץ אחד}one{מתבצע שיתוף של # קבצים}two{מתבצע שיתוף של # קבצים}other{מתבצע שיתוף של # קבצים}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{שיתוף תמונה עם טקסט}one{שיתוף # תמונות עם טקסט}two{שיתוף # תמונות עם טקסט}other{שיתוף # תמונות עם טקסט}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{שיתוף תמונה עם קישור}one{שיתוף # תמונות עם קישור}two{שיתוף # תמונות עם קישור}other{שיתוף # תמונות עם קישור}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{שיתוף סרטון עם טקסט}one{שיתוף # סרטונים עם טקסט}two{שיתוף # סרטונים עם טקסט}other{שיתוף # סרטונים עם טקסט}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"תמונה ממוזערת של תצוגה מקדימה של תמונה"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"תמונה ממוזערת של תצוגה מקדימה של סרטון"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"תמונה ממוזערת של תצוגה מקדימה של קובץ"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"אין אנשים שניתן לשתף איתם"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"רשימת האפליקציות"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"אי אפשר לפתוח את התוכן הזה באמצעות אפליקציות לעבודה"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"אי אפשר לשתף את התוכן הזה עם אפליקציות לשימוש אישי"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"אי אפשר לפתוח את התוכן הזה באמצעות אפליקציות לשימוש אישי"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"פרופיל העבודה מושהה"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"יש להקיש כדי להפעיל את פרופיל העבודה"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"האפליקציות לעבודה מושהות"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ביטול ההשהיה"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"אין אפליקציות לעבודה"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"אין אפליקציות לשימוש אישי"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"לפתוח את <xliff:g id="APP">%s</xliff:g> בפרופיל האישי?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"הכללת הטקסט"</string>      <string name="exclude_link" msgid="1332778255031992228">"החרגת הקישור"</string>      <string name="include_link" msgid="827855767220339802">"הכללת הקישור"</string> +    <string name="pinned" msgid="7623664001331394139">"מוצמד"</string>  </resources> diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml index 0e0751d8..15c2277b 100644 --- a/java/res/values-ja/strings.xml +++ b/java/res/values-ja/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> を固定"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> の固定を解除"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"編集"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name}、他 # ファイル}other{{file_name}、他 # ファイル}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{動画を共有中}other{# 個の動画を共有中}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# 個のアイテムを共有中}other{# 個のアイテムを共有中}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"テキスト付き画像を共有中"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"リンク付き画像を共有中"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{リンク付き画像を共有しています}other{リンク付き画像を # 件共有しています}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{テキスト付き動画を共有中}other{テキスト付き動画を # 件共有中}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"画像のプレビュー サムネイル"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"動画のプレビュー サムネイル"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ファイルのプレビュー サムネイル"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"おすすめの共有相手はいません"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"アプリのリスト"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"このコンテンツを仕事用アプリで開くことはできません"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"このコンテンツを個人用アプリと共有することはできません"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"このコンテンツを個人用アプリで開くことはできません"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"仕事用プロファイルが一時停止しています"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"タップして ON にする"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"仕事用アプリ一時停止中"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"一時停止を解除"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"仕事用アプリはありません"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"個人用アプリはありません"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"個人用プロファイルで <xliff:g id="APP">%s</xliff:g> を開きますか?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"テキストを含める"</string>      <string name="exclude_link" msgid="1332778255031992228">"リンクを除外"</string>      <string name="include_link" msgid="827855767220339802">"リンクを含める"</string> +    <string name="pinned" msgid="7623664001331394139">"固定されています"</string>  </resources> diff --git a/java/res/values-ka/strings.xml b/java/res/values-ka/strings.xml index 5c6e0462..88bc15ac 100644 --- a/java/res/values-ka/strings.xml +++ b/java/res/values-ka/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>-ის ჩამაგრება"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>-ის ჩამაგრების მოხსნა"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"რედაქტირება"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ფაილი}other{{file_name} + # ფაილი}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ფაილი}other{+ # ფაილი}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{ზიარდება ვიდეო}other{ზიარდება # ვიდეო}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{ზიარდება # ერთეული}other{ზიარდება # ერთეული}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"სურათი ზიარდება ტექსტით"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"სურათი ზიარდება ბმულით"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{სურათი ზიარდება ბმულით}other{# სურათი ზიარდება ბმულით}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ვიდეო ზიარდება ტექსტით}other{# ვიდეო ზიარდება ტექსტით}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"სურათის წინასწარი ვერსიის მინიატურა"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ვიდეოს წინასწარი ვერსიის მინიატურა"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ფაილის წინასწარი ვერსიის მინიატურა"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ვერ იძებნება რეკომენდებული ადამიანები, რომლებთანაც გაზიარება შეიძლება"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"აპების სია"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ამ კონტენტის სამსახურის აპებით გახსნა შეუძლებელია"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ამ კონტენტის პირადი აპებისთვის გაზიარება შეუძლებელია"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ამ კონტენტის პირადი აპებით გახსნა შეუძლებელია"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"სამსახურის პროფილი დაპაუზებულია"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"შეეხეთ ჩასართავად"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"სამსახურის აპები დაპაუზებულია"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"პაუზის გაუქმება"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"სამსახურის აპები არ არის"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"პირადი აპები არ არის"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"გსურთ <xliff:g id="APP">%s</xliff:g>-ის გახსნა თქვენს პირად პროფილში?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ტექსტის ჩასმა"</string>      <string name="exclude_link" msgid="1332778255031992228">"ბმულის ამოღება"</string>      <string name="include_link" msgid="827855767220339802">"ბმულის დართვა"</string> +    <string name="pinned" msgid="7623664001331394139">"ჩამაგრებული"</string>  </resources> diff --git a/java/res/values-kk/strings.xml b/java/res/values-kk/strings.xml index 94ff2581..7b195799 100644 --- a/java/res/values-kk/strings.xml +++ b/java/res/values-kk/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> бекіту"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> босату"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Өзгерту"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{Бейне бөлісіліп жатыр}other{# бейне бөлісіліп жатыр}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# элемент бөлісіліп жатыр}other{# элемент бөлісіліп жатыр}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Сурет мәтінімен бөлісіліп жатыр"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Сурет сілтемесімен бөлісіліп жатыр"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Сілтемесі бар сурет жіберу}other{Сілтемесі бар # сурет жіберу}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Мәтіні бар бейне жіберу}other{Мәтіні бар # бейне жіберу}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Алдын ала көрсетілген суреттің нобайы"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Алдын ала көрсетілген бейненің нобайы"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Алдын ала көрсетілген файлдың нобайы"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Бөлісу үшін ұсынылатын адамдар жоқ."</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Қолданбалар тізімі"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бұл контентті жұмыс қолданбаларымен ашу мүмкін емес."</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бұл контентті жеке қолданбалармен бөлісу мүмкін емес."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бұл контентті жеке қолданбалармен ашу мүмкін емес."</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Жұмыс профилі кідіртілді."</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Қосу үшін түртіңіз"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Жұмыс қолданбалары кідіртілген."</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Қайта қосу"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Жұмыс қолданбалары жоқ."</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Жеке қолданбалар жоқ."</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> қолданбасын жеке профиліңізде ашу керек пе?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Мәтін қосу"</string>      <string name="exclude_link" msgid="1332778255031992228">"Сілтемені шығару"</string>      <string name="include_link" msgid="827855767220339802">"Сілтеме қосу"</string> +    <string name="pinned" msgid="7623664001331394139">"Бекітілген"</string>  </resources> diff --git a/java/res/values-km/strings.xml b/java/res/values-km/strings.xml index 9d069d8a..ae956af3 100644 --- a/java/res/values-km/strings.xml +++ b/java/res/values-km/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"ខ្ទាស់ <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"ដកខ្ទាស់ <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"កែ"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ឯកសារ #}other{{file_name} + ឯកសារ #}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ឯកសារ}other{+ # ឯកសារ}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{កំពុងចែករំលែកវីដេអូ}other{កំពុងចែករំលែកវីដេអូ #}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{កំពុងចែករំលែកធាតុ #}other{កំពុងចែករំលែកធាតុ #}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ចែករំលែករូបភាពជាមួយអក្សរ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ចែករំលែករូបភាពជាមួយតំណ"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ចែករំលែករូបភាពជាមួយតំណ}other{ចែករំលែករូបភាព # ជាមួយតំណ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ចែករំលែកវីដេអូជាមួយអក្សរ}other{ចែករំលែក # វីដេអូជាមួយអក្សរ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"រូបក្របតំណាងការមើលរូបភាពសាកល្បង"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"រូបក្របតំណាងការមើលវីដេអូសាកល្បង"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"រូបក្របតំណាងការមើលឯកសារសាកល្បង"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"មិនមានមនុស្សដែលបានណែនាំសម្រាប់ចែករំលែកជាមួយទេ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"បញ្ជីកម្មវិធី"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ខ្លឹមសារនេះមិនអាចចែករំលែកតាមរយៈកម្មវិធីផ្ទាល់ខ្លួនបានទេ"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"មិនអាចចែករំលែកខ្លឹមសារនេះជាមួយកម្មវិធីផ្ទាល់ខ្លួនបានទេ"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ខ្លឹមសារនេះមិនអាចបើកតាមរយៈកម្មវិធីផ្ទាល់ខ្លួនបានទេ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"កម្រងព័ត៌មានការងារត្រូវបានផ្អាក"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ចុចដើម្បីបើក"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"កម្មវិធីការងារត្រូវបានផ្អាក"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ឈប់ផ្អាក"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"គ្មានកម្មវិធីការងារទេ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"គ្មានកម្មវិធីផ្ទាល់ខ្លួនទេ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"បើក <xliff:g id="APP">%s</xliff:g> នៅក្នុងកម្រងព័ត៌មានផ្ទាល់ខ្លួនរបស់អ្នកឬ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"រួមបញ្ចូលអក្សរ"</string>      <string name="exclude_link" msgid="1332778255031992228">"មិនរួមបញ្ចូលតំណ"</string>      <string name="include_link" msgid="827855767220339802">"រួមបញ្ចូលតំណ"</string> +    <string name="pinned" msgid="7623664001331394139">"បានខ្ទាស់"</string>  </resources> diff --git a/java/res/values-kn/strings.xml b/java/res/values-kn/strings.xml index 2e6c0fa8..505277c6 100644 --- a/java/res/values-kn/strings.xml +++ b/java/res/values-kn/strings.xml @@ -36,7 +36,7 @@      <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> -    <string name="whichHomeApplication" msgid="8797832422254564739">"ಮುಖಪುಟ ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ"</string> +    <string name="whichHomeApplication" msgid="8797832422254564739">"Home ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ"</string>      <string name="whichHomeApplicationNamed" msgid="3943122502791761387">"<xliff:g id="APP">%1$s</xliff:g> ಅನ್ನು ಹೋಮ್ ಆಗಿ ಬಳಸಿ"</string>      <string name="whichHomeApplicationLabel" msgid="2066319585322981524">"ಚಿತ್ರ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಿ"</string>      <string name="whichImageCaptureApplication" msgid="7830965894804399333">"ಇದರ ಜೊತೆಗೆ ಚಿತ್ರ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಿ"</string> @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ಪಿನ್ ಮಾಡಿ"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ಅನ್ನು ಅನ್ಪಿನ್ ಮಾಡಿ"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ಎಡಿಟ್"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ಫೈಲ್}one{{file_name} + # ಫೈಲ್ಗಳು}other{{file_name} + # ಫೈಲ್ಗಳು}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ಫೈಲ್}one{+ # ಫೈಲ್ಗಳು}other{+ # ಫೈಲ್ಗಳು}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ಇನ್ನಷ್ಟು ಫೈಲ್}one{+ # ಇನ್ನಷ್ಟು ಫೈಲ್ಗಳು}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{# ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ಐಟಂ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ಐಟಂಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಐಟಂಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ಪಠ್ಯದೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ಲಿಂಕ್ನೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಫೈಲ್ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ಲಿಂಕ್ನೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್ನೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್ನೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ಚಿತ್ರ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್ನೇಲ್"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ವೀಡಿಯೊ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್ನೇಲ್"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ಫೈಲ್ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್ನೇಲ್"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ಹಂಚಿಕೊಳ್ಳಲು ಶಿಫಾರಸು ಮಾಡಲಾದವರು ಯಾರೂ ಇಲ್ಲ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ಆ್ಯಪ್ಗಳ ಪಟ್ಟಿ"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ಗಳ ಈ ವಿಷಯವನ್ನು ತೆರೆಯಲಾಗುವುದಿಲ್ಲ"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್ಗಳ ಮೂಲಕ ಈ ವಿಷಯವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುವುದಿಲ್ಲ"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್ಗಳ ಮೂಲಕ ಈ ವಿಷಯವನ್ನು ತೆರೆಯಲಾಗುವುದಿಲ್ಲ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಪ್ರೊಫೈಲ್ ಅನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ಆನ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ಗಳನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ವಿರಾಮವನ್ನು ರದ್ದುಗೊಳಿಸಿ"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ಯಾವುದೇ ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ಗಳಿಲ್ಲ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ಯಾವುದೇ ವೈಯಕ್ತಿಕ ಆ್ಯಪ್ಗಳಿಲ್ಲ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ನಿಮ್ಮ ವೈಯಕ್ತಿಕ ಪ್ರೊಫೈಲ್ನಲ್ಲಿ <xliff:g id="APP">%s</xliff:g> ಅನ್ನು ತೆರೆಯಬೇಕೆ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ಪಠ್ಯವನ್ನು ಸೇರಿಸಿ"</string>      <string name="exclude_link" msgid="1332778255031992228">"ಲಿಂಕ್ ಹೊರತುಪಡಿಸಿ"</string>      <string name="include_link" msgid="827855767220339802">"ಲಿಂಕ್ ಸೇರಿಸಿ"</string> +    <string name="pinned" msgid="7623664001331394139">"ಪಿನ್ ಮಾಡಲಾಗಿದೆ"</string>  </resources> diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml index 4df2adff..e9e908be 100644 --- a/java/res/values-ko/strings.xml +++ b/java/res/values-ko/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> 고정"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> 고정 해제"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"수정"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + 파일 #개}other{{file_name} + 파일 #개}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{동영상 1개 공유 중}other{동영상 #개 공유 중}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{항목 1개 공유 중}other{항목 #개 공유 중}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"텍스트로 이미지 공유 중"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"링크로 이미지 공유 중"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{링크로 이미지 공유 중}other{링크로 이미지 #개 공유 중}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{텍스트로 동영상 공유 중}other{텍스트로 동영상 #개 공유 중}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"이미지 미리보기 썸네일"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"동영상 미리보기 썸네일"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"파일 미리보기 썸네일"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"공유할 추천 사용자가 없음"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"앱 목록"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"이 콘텐츠는 직장 앱으로 열 수 없습니다."</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"이 콘텐츠는 개인 앱을 통해 공유할 수 없습니다."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"이 콘텐츠는 개인 앱으로 열 수 없습니다."</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"직장 프로필이 일시중지됨"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"탭하여 사용 설정"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"직장 앱이 일시중지됨"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"일시중지 해제"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"직장 앱 없음"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"개인 앱 없음"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"개인 프로필에서 <xliff:g id="APP">%s</xliff:g> 앱을 여시겠습니까?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"텍스트 포함"</string>      <string name="exclude_link" msgid="1332778255031992228">"링크 제외"</string>      <string name="include_link" msgid="827855767220339802">"링크 포함"</string> +    <string name="pinned" msgid="7623664001331394139">"고정됨"</string>  </resources> diff --git a/java/res/values-ky/strings.xml b/java/res/values-ky/strings.xml index c438a92f..311a2169 100644 --- a/java/res/values-ky/strings.xml +++ b/java/res/values-ky/strings.xml @@ -53,29 +53,38 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Кадап коюу: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> бошотуу"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Түзөтүү"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файл}}"</string> +    <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_items" msgid="5266543892527310331">"{count,plural, =1{# нерсе бөлүшүлүүдө}other{# нерсе бөлүшүлүүдө}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Сүрөттү текст менен жөнөтүү"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Сүрөттү шилтеме менен жөнөтүү"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файл бөлүшүлүүдө}other{# файл бөлүшүлүүдө}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Сүрөттү текст менен жөнөтүү}other{# cүрөттү текст менен жөнөтүү}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Сүрөттү шилтеме менен жөнөтүү}other{# сүрөттү шилтеме менен жөнөтүү}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Видеону текст менен жөнөтүү}other{# видеону текст менен жөнөтүү}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Сүрөттүн алдын ала эскизи"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Видеонун алдын ала эскизи"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Файлдын алдын ала эскизи"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Бөлүшкөнгө эч ким сунушталган жок"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Колдонмолордун тизмеси"</string>      <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_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көрүнүш"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жумуш көрүнүшү"</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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бул мазмунду жеке колдонмолор менен бөлүшүү мүмкүн эмес"</string> -    <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бул мазмунду жеке колдонмолор менен ачуу мүмкүн эмес"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Жумуш профили тындырылган"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Күйгүзүү үчүн таптап коюңуз"</string> +    <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Бул нерсени жумуш колдонмолору менен бөлүшө албайсыз"</string> +    <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бул нерсени жумуш колдонмолору менен ача албайсыз"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бул нерсени жеке колдонмолор менен бөлүшө албайсыз"</string> +    <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бул нерсени жеке колдонмолор менен ача албайсыз"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Жумуш колдонмолору тындырылды"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Иштетүү"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Жумуш колдонмолору жок"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Жеке колдонмолор жок"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> колдонмосу жеке профилде ачылсынбы?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Текст кошуу"</string>      <string name="exclude_link" msgid="1332778255031992228">"Шилтемени чыгарып салуу"</string>      <string name="include_link" msgid="827855767220339802">"Шилтеме кошуу"</string> +    <string name="pinned" msgid="7623664001331394139">"Кадалган"</string>  </resources> diff --git a/java/res/values-lo/strings.xml b/java/res/values-lo/strings.xml index debe9c23..48e9a074 100644 --- a/java/res/values-lo/strings.xml +++ b/java/res/values-lo/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"ປັກໝຸດ <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"ຖອດປັກມຸດ <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ແກ້ໄຂ"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ໄຟລ໌}other{{file_name} + # ໄຟລ໌}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{ອີກ # ໄຟລ໌}other{ອີກ # ໄຟລ໌}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{ກຳລັງແບ່ງປັນ # ລາຍການ}other{ກຳລັງແບ່ງປັນ # ລາຍການ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ກຳລັງແບ່ງປັນຮູບດ້ວຍຂໍ້ຄວາມ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ກຳລັງແບ່ງປັນຮູບດ້ວຍລິ້ງ"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ກຳລັງແບ່ງປັນຮູບພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ຮູບພ້ອມລິ້ງ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອພ້ອມຂໍ້ຄວາມ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອພ້ອມຂໍ້ຄວາມ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ຮູບຕົວຢ່າງຂອງຮູບພາບ"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ຮູບຕົວຢ່າງຂອງວິດີໂອ"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ຮູບຕົວຢ່າງຂອງໄຟລ໌"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ບໍ່ມີຄົນທີ່ແນະນຳໃຫ້ແບ່ງປັນນຳ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ລາຍຊື່ແອັບ"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກເປີດໄດ້ດ້ວຍແອັບບ່ອນເຮັດວຽກ"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກແບ່ງປັນກັບແອັບສ່ວນຕົວໄດ້"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກເປີດໄດ້ດ້ວຍແອັບສ່ວນຕົວ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ຢຸດໂປຣໄຟລ໌ວຽກໄວ້ຊົ່ວຄາວແລ້ວ"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ແຕະເພື່ອເປີດໃຊ້"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ຢຸດແອັບບ່ອນເຮັດວຽກໄວ້ຊົ່ວຄາວແລ້ວ"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ຍົກເລີກການຢຸດຊົ່ວຄາວ"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ບໍ່ມີແອັບບ່ອນເຮັດວຽກ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ບໍ່ມີແອັບສ່ວນຕົວ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ເປີດ <xliff:g id="APP">%s</xliff:g> ໃນໂປຣໄຟລ໌ສ່ວນຕົວຂອງທ່ານບໍ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ຮວມຂໍ້ຄວາມ"</string>      <string name="exclude_link" msgid="1332778255031992228">"ບໍ່ຮວມລິ້ງ"</string>      <string name="include_link" msgid="827855767220339802">"ຮວມລິ້ງ"</string> +    <string name="pinned" msgid="7623664001331394139">"ປັກໝຸດແລ້ວ"</string>  </resources> diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml index 77ae0a47..51ffbbff 100644 --- a/java/res/values-lt/strings.xml +++ b/java/res/values-lt/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Prisegti <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Atsegti <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Redaguoti"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{„{file_name}“ ir dar # failas}one{„{file_name}“ ir dar # failas}few{„{file_name}“ ir dar # failai}many{„{file_name}“ ir dar # failo}other{„{file_name}“ ir dar # failų}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Dar # failas}one{Dar # failas}few{Dar # failai}many{Dar # failo}other{Dar # failų}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Dar # failas}one{Dar # failas}few{Dar # failai}many{Dar # failo}other{Dar # failų}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Bendrinamas tekstas"</string>      <string name="sharing_link" msgid="2307694372813942916">"Bendrinama nuoroda"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Bendrinamas vaizdas}one{Bendrinamas # vaizdas}few{Bendrinami # vaizdai}many{Bendrinama # vaizdo}other{Bendrinama # vaizdų}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Bendrinamas vaizdo įrašas}one{Bendrinamas # vaizdo įrašas}few{Bendrinami # vaizdo įrašai}many{Bendrinama # vaizdo įrašo}other{Bendrinama # vaizdo įrašų}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Bendrinamas # elementas}one{Bendrinamas # elementas}few{Bendrinami # elementai}many{Bendrinama # elemento}other{Bendrinama # elementų}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Bendrinamas vaizdas su tekstu"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Bendrinamas vaizdas su nuoroda"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Bendrinamas # failas}one{Bendrinamas # failas}few{Bendrinami # failai}many{Bendrinama # failo}other{Bendrinama # failų}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Bendrinamas vaizdas su tekstu}one{Bendrinamas # vaizdas su tekstu}few{Bendrinami # vaizdai su tekstu}many{Bendrinama # vaizdo su tekstu}other{Bendrinama # vaizdų su tekstu}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bendrinamas vaizdas su nuoroda}one{Bendrinamas # vaizdas su nuoroda}few{Bendrinami # vaizdai su nuoroda}many{Bendrinama # vaizdo su nuoroda}other{Bendrinama # vaizdų su nuoroda}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Bendrinamas vaizdo įrašas su tekstu}one{Bendrinamas # vaizdo įrašas su tekstu}few{Bendrinami # vaizdo įrašai su tekstu}many{Bendrinama # vaizdo įrašo su tekstu}other{Bendrinama # vaizdo įrašų su tekstu}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Vaizdo peržiūros miniatiūra"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Vaizdo įrašo peržiūros miniatiūra"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Failo peržiūros miniatiūra"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nėra rekomenduojamų žmonių, su kuriais būtų galima bendrinti"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Programų sąrašas"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šio turinio negalima atidaryti naudojant darbo programas"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Šio turinio negalima bendrinti su asmeninėmis programomis"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Šio turinio negalima atidaryti naudojant asmenines programas"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Darbo profilis pristabdytas"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Paliesti, norint įjungti"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Darbo programos pristabdytos"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Atšaukti pristabdymą"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nėra darbo programų"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nėra asmeninių programų"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Atidaryti „<xliff:g id="APP">%s</xliff:g>“ asmeniniame profilyje?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Įtraukti tekstą"</string>      <string name="exclude_link" msgid="1332778255031992228">"Išskirti nuorodą"</string>      <string name="include_link" msgid="827855767220339802">"Įtraukti nuorodą"</string> +    <string name="pinned" msgid="7623664001331394139">"Prisegta"</string>  </resources> diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml index 6fb7fee3..de5c352b 100644 --- a/java/res/values-lv/strings.xml +++ b/java/res/values-lv/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Piespraust lietotni <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Atspraust lietotni <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Rediģēt"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} un vēl # fails}zero{{file_name} un vēl # failu}one{{file_name} un vēl # fails}other{{file_name} un vēl # faili}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{un vēl # fails}zero{un vēl # faili}one{un vēl # fails}other{un vēl # faili}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Un vēl # fails}zero{Un vēl # failu}one{Un vēl # fails}other{Un vēl # faili}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Tiek kopīgots teksts"</string>      <string name="sharing_link" msgid="2307694372813942916">"Tiek kopīgota saite"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Tiek kopīgots attēls}zero{Tiek kopīgoti # attēli}one{Tiek kopīgots # attēls}other{Tiek kopīgoti # attēli}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Tiek kopīgots video}zero{Tiek kopīgoti # video}one{Tiek kopīgots # video}other{Tiek kopīgoti # video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Tiek kopīgots # vienums}zero{Tiek kopīgoti # vienumi}one{Tiek kopīgots # vienums}other{Tiek kopīgoti # vienumi}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Tiek kopīgots attēls ar tekstu"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Tiek kopīgots attēls ar saiti"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Notiek # faila kopīgošana}zero{Notiek # failu kopīgošana}one{Notiek # faila kopīgošana}other{Notiek # failu kopīgošana}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Tiek kopīgots attēls ar tekstu}zero{Tiek kopīgoti # attēli ar tekstu}one{Tiek kopīgots # attēls ar tekstu}other{Tiek kopīgoti # attēli ar tekstu}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Tiek kopīgots attēls ar saiti}zero{Tiek kopīgoti # attēli ar saitēm}one{Tiek kopīgots # attēls ar saitēm}other{Tiek kopīgoti # attēli ar saitēm}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Tiek kopīgots videoklips ar tekstu}zero{Tiek kopīgoti # videoklipi ar tekstu}one{Tiek kopīgots # videoklips ar tekstu}other{Tiek kopīgoti # videoklipi ar tekstu}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Attēla priekšskatījuma sīktēls"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Videoklipa priekšskatījuma sīktēls"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faila priekšskatījuma sīktēls"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nav ieteikta neviena persona, ar ko kopīgot"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lietotņu saraksts"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šo saturu nevar atvērt darba lietotnēs"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Šo saturu nevar kopīgot ar personīgajām lietotnēm"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Šo saturu nevar atvērt personīgajās lietotnēs"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Darba profila darbība ir apturēta."</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Lai ieslēgtu, pieskarieties"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Darba lietotnes ir apturētas."</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Aktivizēt"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nav darba lietotņu"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nav personīgu lietotņu"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vai atvērt lietotni <xliff:g id="APP">%s</xliff:g> jūsu personīgajā profilā?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Iekļaut tekstu"</string>      <string name="exclude_link" msgid="1332778255031992228">"Izslēgt saiti"</string>      <string name="include_link" msgid="827855767220339802">"Iekļaut saiti"</string> +    <string name="pinned" msgid="7623664001331394139">"Piespraustās"</string>  </resources> diff --git a/java/res/values-mk/strings.xml b/java/res/values-mk/strings.xml index 001772fa..7ef3a9ca 100644 --- a/java/res/values-mk/strings.xml +++ b/java/res/values-mk/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Закачи <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Откачи <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Измени"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # датотека}one{{file_name} + # датотека}other{{file_name} + # датотеки}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # датотека}one{+ # датотека}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{Се споделува # слика}other{Се споделуваат # слики}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{и уште # датотека}one{и уште # датотека}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{Споделување # слика}other{Споделување # слики}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Се споделува видео}one{Се споделува # видео}other{Се споделуваат # видеа}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Се споделува # ставка}one{Се споделува # ставка}other{Се споделуваат # ставки}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Се споделува слика со текст"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Се споделува слика со линк"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Се споделува # датотека}one{Се споделуваат # датотека}other{Се споделуваат # датотеки}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Се споделува слика со SMS}one{Се споделуваат # слика со SMS}other{Се споделуваат # слики со SMS}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Се споделува слика со линк}one{Се споделуваат # слика со линк}other{Се споделуваат # слики со линк}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Се споделува видео со SMS}one{Се споделуваат # видео со SMS}other{Се споделуваат # видеа со SMS}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Сликичка за преглед на сликата"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Сликичка за преглед на видеото"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Сликичка за преглед на датотеката"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Нема препорачани луѓе со кои може да се сподели"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список со апликации"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овие содржини не може да се отвораат со работни апликации"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Овие содржини не може да се споделуваат со лични апликации"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Овие содржини не може да се отвораат со лични апликации"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Работниот профил е паузиран"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Допрете за да вклучите"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Работните апликации се паузирани"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Прекини ја паузата"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Нема работни апликации"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Нема лични апликации"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Да се отвори <xliff:g id="APP">%s</xliff:g> во личниот профил?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Опфати текст"</string>      <string name="exclude_link" msgid="1332778255031992228">"Исклучи линк"</string>      <string name="include_link" msgid="827855767220339802">"Вклучи линк"</string> +    <string name="pinned" msgid="7623664001331394139">"Закачено"</string>  </resources> diff --git a/java/res/values-ml/strings.xml b/java/res/values-ml/strings.xml index b91adae8..03b01db9 100644 --- a/java/res/values-ml/strings.xml +++ b/java/res/values-ml/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> പിൻ ചെയ്യുക"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> അൺപിൻ ചെയ്യുക"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"എഡിറ്റ് ചെയ്യുക"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ഫയൽ}other{{file_name} + # ഫയലുകൾ}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ഫയൽ}other{+ # ഫയലുകൾ}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{വീഡിയോ പങ്കിടുന്നു}other{# വീഡിയോകൾ പങ്കിടുന്നു}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ഇനം പങ്കിടുന്നു}other{# ഇനങ്ങൾ പങ്കിടുന്നു}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ടെക്സ്റ്റിനൊപ്പം ചിത്രം പങ്കിടുന്നു"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ലിങ്കിനൊപ്പം ചിത്രം പങ്കിടുന്നു"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ലിങ്കിനൊപ്പം ചിത്രം പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # ചിത്രങ്ങൾ പങ്കിടുന്നു}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ടെക്സ്റ്റിനൊപ്പം വീഡിയോ പങ്കിടുന്നു}other{ടെക്സ്റ്റിനൊപ്പം # വീഡിയോകൾ പങ്കിടുന്നു}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ചിത്രത്തിന്റെ പ്രിവ്യൂ ലഘുചിത്രം"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"വീഡിയോയുടെ പ്രിവ്യൂ ലഘുചിത്രം"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ഫയലിന്റെ പ്രിവ്യൂ ലഘുചിത്രം"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"പങ്കിടാൻ, നിർദ്ദേശിക്കപ്പെട്ട ആളുകളൊന്നുമില്ല"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ആപ്പുകളുടെ ലിസ്റ്റ്"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ഔദ്യോഗിക ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം തുറക്കാനാകില്ല"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"വ്യക്തിപര ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം പങ്കിടാനാകില്ല"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"വ്യക്തിപര ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം തുറക്കാനാകില്ല"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ഔദ്യോഗിക പ്രൊഫൈൽ തൽക്കാലം നിർത്തിയിരിക്കുന്നു"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ഓണാക്കാൻ ടാപ്പ് ചെയ്യുക"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ഔദ്യോഗിക ആപ്പുകൾ തൽക്കാലം നിർത്തിയിരിക്കുന്നു"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"താൽക്കാലികമായി നിർത്തിയത് മാറ്റുക"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ഔദ്യോഗിക ആപ്പുകൾ ഇല്ല"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"വ്യക്തിപര ആപ്പുകൾ ഇല്ല"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>, നിങ്ങളുടെ വ്യക്തിപരമായ പ്രൊഫൈലിൽ തുറക്കണോ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ടെക്സ്റ്റ് ഉൾപ്പെടുത്തുക"</string>      <string name="exclude_link" msgid="1332778255031992228">"ലിങ്ക് ഒഴിവാക്കുക"</string>      <string name="include_link" msgid="827855767220339802">"ലിങ്ക് ഉൾപ്പെടുത്തുക"</string> +    <string name="pinned" msgid="7623664001331394139">"പിൻ ചെയ്തത്"</string>  </resources> diff --git a/java/res/values-mn/strings.xml b/java/res/values-mn/strings.xml index ad356c08..339ca5e4 100644 --- a/java/res/values-mn/strings.xml +++ b/java/res/values-mn/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>-г бэхлэх"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>-г тогтоосныг болиулах"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Засах"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файл}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Видео хуваалцаж байна}other{# видео хуваалцаж байна}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# зүйл хуваалцаж байна}other{# зүйл хуваалцаж байна}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Тексттэй зураг хуваалцаж байна"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Холбоостой зураг хуваалцаж байна"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Холбоостой зураг хуваалцаж байна}other{Холбоостой # зураг хуваалцаж байна}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Тексттэй видео хуваалцаж байна}other{Тексттэй # видео хуваалцаж байна}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Зургийн урьдчилан үзэх өнгөц зураг"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Видеоны урьдчилан үзэх өнгөц зураг"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Файлын урьдчилан үзэх өнгөц зураг"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Хуваалцахыг санал болгосон хүн байхгүй"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Аппын жагсаалт"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Энэ контентыг ажлын аппуудаар нээх боломжгүй"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Энэ контентыг хувийн аппуудаар хуваалцах боломжгүй"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Энэ контентыг хувийн аппуудаар нээх боломжгүй"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Ажлын профайлыг түр зогсоосон"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Асаахын тулд товших"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ажлын аппуудыг түр зогсоосон"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Үргэлжлүүлэх"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ямар ч ажлын апп байхгүй байна"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ямар ч хувийн апп байхгүй байна"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Хувийн профайл дээрээ <xliff:g id="APP">%s</xliff:g>-г нээх үү?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Текстийг оруулах"</string>      <string name="exclude_link" msgid="1332778255031992228">"Холбоосыг хасах"</string>      <string name="include_link" msgid="827855767220339802">"Холбоосыг оруулах"</string> +    <string name="pinned" msgid="7623664001331394139">"Бэхэлсэн"</string>  </resources> diff --git a/java/res/values-mr/strings.xml b/java/res/values-mr/strings.xml index 469adb4b..5202a3b7 100644 --- a/java/res/values-mr/strings.xml +++ b/java/res/values-mr/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> पिन करा"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ला अनपिन करा"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"संपादित करा"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फाइल}other{{file_name} + # फाइल}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # फाइल}other{+ # फाइल}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{व्हिडिओ शेअर करत आहे}other{# व्हिडिओ शेअर करत आहे}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# आयटम शेअर करत आहे}other{# आयटम शेअर करत आहे}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"मजकुरासह इमेज शेअर करत आहे"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंकसह इमेज शेअर करत आहे"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंकसह इमेज शेअर करत आहे}other{लिंकसह # इमेज शेअर करत आहे}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{मजकुरासह व्हिडिओ शेअर करत आहे}other{मजकुरासह # व्हिडिओ शेअर करत आहे}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"इमेज पूर्वावलोकनाची थंबनेल"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"व्हिडिओ पूर्वावलोकनाची थंबनेल"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"फाइल पूर्वावलोकनाची थंबनेल"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"शेअर करण्यासाठी शिफारस केलेल्या कोणत्याही व्यक्ती नाहीत"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"अॅप्स सूची"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"हा आशय कार्य ॲप्स वापरून उघडला जाऊ शकत नाही"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"हा आशय वैयक्तिक ॲप्ससह शेअर केला जाऊ शकत नाही"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"हा आशय वैयक्तिक ॲप्स वापरून उघडला जाऊ शकत नाही"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"कार्य प्रोफाइल थांबवली आहे"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"सुरू करण्यासाठी टॅप करा"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"कामाशी संबंधित अॅप्स थांबवली आहेत"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"पुन्हा सुरू करा"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"कोणतीही कार्य ॲप्स सपोर्ट करत नाहीत"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"कोणतीही वैयक्तिक ॲप्स सपोर्ट करत नाहीत"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"तुमच्या वैयक्तिक प्रोफाइलमध्ये <xliff:g id="APP">%s</xliff:g> उघडायचे आहे का?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"मजकूर समाविष्ट करा"</string>      <string name="exclude_link" msgid="1332778255031992228">"लिंक वगळा"</string>      <string name="include_link" msgid="827855767220339802">"लिंक समाविष्ट करा"</string> +    <string name="pinned" msgid="7623664001331394139">"पिन केलेली"</string>  </resources> diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml index 4d6eb7ca..f1ac4d1d 100644 --- a/java/res/values-ms/strings.xml +++ b/java/res/values-ms/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Sematkan <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Nyahsemat <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fail}other{{file_name} + # fail}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fail}other{+ # fail}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fail lagi}other{+ # fail lagi}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Berkongsi teks"</string>      <string name="sharing_link" msgid="2307694372813942916">"Berkongsi pautan"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Berkongsi imej}other{Berkongsi # imej}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Berkongsi video}other{Berkongsi # video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Berkongsi # item}other{Berkongsi # item}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Berkongsi imej dengan teks"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Berkongsi imej dengan pautan"</string> -    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tiada orang yang disyorkan untuk berkongsi"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Senarai apl"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Berkongsi # fail}other{Berkongsi # fail}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Berkongsi imej dengan teks}other{Berkongsi # imej dengan teks}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Berkongsi imej dengan pautan}other{Berkongsi # imej dengan pautan}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Berkongsi video dengan teks}other{Berkongsi # video dengan teks}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Lakaran kecil pratonton imej"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Lakaran kecil pratonton video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Lakaran kecil pratonton fail"</string> +    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tiada orang yang disyorkan untuk membuat perkongsian"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kandungan ini tidak boleh dibuka dengan apl kerja"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kandungan ini tidak boleh dikongsi dengan apl peribadi"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Kandungan ini tidak boleh dibuka dengan apl peribadi"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil kerja dijeda"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ketik untuk menghidupkan profil"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Apl kerja dijeda"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Nyahjeda"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Tiada apl kerja"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Tiada apl peribadi"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buka <xliff:g id="APP">%s</xliff:g> dalam profil peribadi anda?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Sertakan teks"</string>      <string name="exclude_link" msgid="1332778255031992228">"Kecualikan pautan"</string>      <string name="include_link" msgid="827855767220339802">"Sertakan pautan"</string> +    <string name="pinned" msgid="7623664001331394139">"Disemat"</string>  </resources> diff --git a/java/res/values-my/strings.xml b/java/res/values-my/strings.xml index 0b175357..c3ab1ee2 100644 --- a/java/res/values-my/strings.xml +++ b/java/res/values-my/strings.xml @@ -29,7 +29,7 @@      <string name="whichGiveAccessToApplicationLabel" msgid="5120142857844152131">"သုံးခွင့်ပေးရန်"</string>      <string name="whichEditApplication" msgid="5097563012157950614">"...နှင့် တည်းဖြတ်ရန်"</string>      <string name="whichEditApplicationNamed" msgid="3150137489226219100">"<xliff:g id="APP">%1$s</xliff:g> ဖြင့် တည်းဖြတ်ခြင်း"</string> -    <string name="whichEditApplicationLabel" msgid="5992662938338600364">"တည်းဖြတ်ပါ"</string> +    <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> @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ကို ပင်ထိုးရန်"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ကို ပင်ဖြုတ်ရန်"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"တည်းဖြတ်ရန်"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ဖိုင်}other{{file_name} + # ဖိုင်}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ဖိုင်}other{+ # ဖိုင်}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{ဗီဒီယို မျှဝေနေသည်}other{ဗီဒီယို # ခု မျှဝေနေသည်}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ခု မျှဝေနေသည်}other{# ခု မျှဝေနေသည်}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"စာပါသောပုံ မျှဝေနေသည်"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"လင့်ခ်ပါသောပုံ မျှဝေနေသည်"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{လင့်ခ်ပါသောပုံကို မျှဝေနေသည်}other{လင့်ခ်ပါသောပုံ # ပုံကို မျှဝေနေသည်}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{စာသားပါသောဗီဒီယိုကို မျှဝေနေသည်}other{စာသားပါသောဗီဒီယို # ခုကို မျှဝေနေသည်}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ပုံအစမ်းကြည့်ရှုမှု ပုံသေး"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ဗီဒီယိုအစမ်းကြည့်ရှုမှု ပုံသေး"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ဖိုင်အစမ်းကြည့်ရှုမှု ပုံသေး"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"မျှဝေရန် အကြံပြုထားသူများ မရှိပါ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"အက်ပ်စာရင်း"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ဤအကြောင်းအရာကို အလုပ်သုံးအက်ပ်များဖြင့် မဖွင့်နိုင်ပါ"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ဤအကြောင်းအရာကို ကိုယ်ပိုင်အက်ပ်များဖြင့် မမျှဝေနိုင်ပါ"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ဤအကြောင်းအရာကို ကိုယ်ပိုင်အက်ပ်များဖြင့် မဖွင့်နိုင်ပါ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"အလုပ်ပရိုဖိုင် ခဏရပ်ထားသည်"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ဖွင့်ရန်တို့ပါ"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"အလုပ်သုံးအက်ပ်များကို ခေတ္တရပ်ထားသည်"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ပြန်ဖွင့်ရန်"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"အလုပ်သုံးအက်ပ်များ မရှိပါ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ကိုယ်ပိုင်အက်ပ်များ မရှိပါ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ကို သင့်ကိုယ်ပိုင်ပရိုဖိုင်တွင် ဖွင့်မလား။"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"စာသားထည့်သွင်းရန်"</string>      <string name="exclude_link" msgid="1332778255031992228">"လင့်ခ် ဖယ်ထုတ်ရန်"</string>      <string name="include_link" msgid="827855767220339802">"လင့်ခ်ထည့်သွင်းရန်"</string> +    <string name="pinned" msgid="7623664001331394139">"ပင်ထိုးထားသည်"</string>  </resources> diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml index b6e49cd2..a2c6da68 100644 --- a/java/res/values-nb/strings.xml +++ b/java/res/values-nb/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fest <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Løsne <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Endre"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}other{{file_name} + # filer}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}other{+ # filer}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fil til}other{+ # filer til}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deler teksten"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deler linken"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deler bildet}other{Deler # bilder}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deler videoen}other{Deler # videoer}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deler # element}other{Deler # elementer}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deler bildet med tekst"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deler bildet med link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deler # fil}other{Deler # filer}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deler bildet med tekst}other{Deler # bilder med tekst}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deler bildet med link}other{Deler # bilder med link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deler videoen med tekst}other{Deler # videoer med tekst}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatyrbilde for forhåndsvisning av bilde"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatyrbilde for forhåndsvisning av video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatyrbilde for forhåndsvisning av fil"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Det finnes ingen anbefalte personer å dele med"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Appliste"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette innholdet kan ikke åpnes med jobbapper"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Dette innholdet kan ikke deles med personlige apper"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Dette innholdet kan ikke åpnes med personlige apper"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Jobbprofilen er satt på pause"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Trykk for å slå på"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Jobbapper er satt på pause"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Slå av pausen"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ingen jobbapper"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ingen personlige apper"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åpne <xliff:g id="APP">%s</xliff:g> i den personlige profilen din?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inkluder teksten"</string>      <string name="exclude_link" msgid="1332778255031992228">"Ekskluder linken"</string>      <string name="include_link" msgid="827855767220339802">"Inkluder linken"</string> +    <string name="pinned" msgid="7623664001331394139">"Festet"</string>  </resources> diff --git a/java/res/values-ne/strings.xml b/java/res/values-ne/strings.xml index 9bf20518..176067f2 100644 --- a/java/res/values-ne/strings.xml +++ b/java/res/values-ne/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> पिन गर्नुहोस्"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> लाई अनपिन गर्नुहोस्"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"सम्पादन गर्नुहोस्"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फाइल}other{{file_name} + # वटा फाइल}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{# भन्दा बढी फाइल}other{# भन्दा बढी फाइलहरू}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{भिडियो सेयर गरिँदै छ}other{# वटा भिडियो सेयर गरिँदै छ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# सामग्री सेयर गरिँदै छ}other{# वटा सामग्री सेयर गरिँदै छ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"टेक्स्ट भएको फोटो सेयर गरिँदै छ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंक भएको फोटो सेयर गरिँदै छ"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंक भएको फोटो सेयर गरिँदै छ}other{लिंक भएका # वटा फोटो सेयर गरिँदै छन्}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{टेक्स्ट भएको भिडियो सेयर गरिँदै छ}other{टेक्स्ट भएका # वटा भिडियो सेयर गरिँदै छन्}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"फोटो प्रिभ्यू थम्बनेल"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"भिडियो प्रिभ्यू थम्बनेल"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"फाइल प्रिभ्यू थम्बनेल"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"कुनै पनि व्यक्तिसँग सेयर गर्ने सिफारिस गरिएको छैन"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"अनुप्रयोगहरूको सूची"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"यो सामग्री कामसम्बन्धी एपहरूमार्फत खोल्न मिल्दैन"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"यो सामग्री व्यक्तिगत एपहरूमार्फत सेयर गर्न मिल्दैन"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"यो सामग्री व्यक्तिगत एपहरूमार्फत खोल्न मिल्दैन"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"कार्य प्रोफाइल पज गरिएको छ"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"अन गर्न ट्याप गर्नुहोस्"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"कामसम्बन्धी एपहरू पज गरिएका छन्"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"अनपज गर्नुहोस्"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"यो सामग्री खोल्न मिल्ने कुनै पनि कामसम्बन्धी एप छैन"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"यो सामग्री खोल्न मिल्ने कुनै पनि व्यक्तिगत एप छैन"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> तपाईंको व्यक्तिगत प्रोफाइलमा खोल्ने हो?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"टेक्स्ट समावेश गर्नुहोस्"</string>      <string name="exclude_link" msgid="1332778255031992228">"लिंक हटाउनुहोस्"</string>      <string name="include_link" msgid="827855767220339802">"लिंक समावेश गर्नुहोस्"</string> +    <string name="pinned" msgid="7623664001331394139">"पिन गरिएको"</string>  </resources> diff --git a/java/res/values-night/styles.xml b/java/res/values-night/styles.xml new file mode 100644 index 00000000..95071bac --- /dev/null +++ b/java/res/values-night/styles.xml @@ -0,0 +1,22 @@ +<!-- +  ~ 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. +  --> +<resources xmlns:android="http://schemas.android.com/apk/res/android" +        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + +    <style name="Theme.DeviceDefault.Resolver" parent="Theme.DeviceDefault.ResolverCommon"> +        <item name="android:windowLightNavigationBar">false</item> +    </style> +</resources> diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml index a779bf68..7ef1513b 100644 --- a/java/res/values-nl/strings.xml +++ b/java/res/values-nl/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> vastzetten"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> losmaken"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Bewerken"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # bestand}other{{file_name} + # bestanden}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # bestand}other{+ # bestanden}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ nog # bestand}other{+ nog # bestanden}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Tekst delen"</string>      <string name="sharing_link" msgid="2307694372813942916">"Link delen"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Afbeelding delen}other{# afbeeldingen delen}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video delen}other{# video\'s delen}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# item delen}other{# items delen}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Afbeelding delen met tekst"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Afbeelding delen met link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# bestand delen}other{# bestanden delen}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Afbeelding met tekst wordt gedeeld}other{# afbeeldingen met tekst worden gedeeld}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Afbeelding delen via link}other{# afbeeldingen delen via link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Video delen via tekstbericht}other{# video\'s delen via tekstbericht}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Voorbeeldthumbnail voor afbeelding"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Voorbeeldthumbnail voor video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Voorbeeldthumbnail voor bestand"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Geen aanbevolen mensen om mee te delen"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lijst met apps"</string>      <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> @@ -74,8 +83,8 @@      <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="6464225110988983641">"Werkprofiel is onderbroken"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tik om aan te zetten"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werk-apps zijn onderbroken"</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>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> openen in je persoonlijke profiel?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Tekst opnemen"</string>      <string name="exclude_link" msgid="1332778255031992228">"Link uitsluiten"</string>      <string name="include_link" msgid="827855767220339802">"Link opnemen"</string> +    <string name="pinned" msgid="7623664001331394139">"Vastgezet"</string>  </resources> diff --git a/java/res/values-or/strings.xml b/java/res/values-or/strings.xml index 0ed83589..93c60db2 100644 --- a/java/res/values-or/strings.xml +++ b/java/res/values-or/strings.xml @@ -30,7 +30,7 @@      <string name="whichEditApplication" msgid="5097563012157950614">"ସହିତ ଏଡିଟ କରନ୍ତୁ"</string>      <string name="whichEditApplicationNamed" msgid="3150137489226219100">"<xliff:g id="APP">%1$s</xliff:g> ମାଧ୍ୟମରେ ଏଡିଟ କରନ୍ତୁ"</string>      <string name="whichEditApplicationLabel" msgid="5992662938338600364">"ଏଡିଟ କରନ୍ତୁ"</string> -    <string name="whichSendApplication" msgid="59510564281035884">"ସେୟାର୍ କରନ୍ତୁ"</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="whichSendToApplication" msgid="2724450540348806267">"ଏହା ଜରିଆରେ ପଠାନ୍ତୁ"</string> @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>କୁ ପିନ କରନ୍ତୁ"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ରେ ଅନ୍ପିନ୍ କରନ୍ତୁ"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ଏଡିଟ କରନ୍ତୁ"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + #ଟି ଫାଇଲ}other{{file_name} + #ଟି ଫାଇଲ}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ #ଟି ଫାଇଲ}other{+ #ଟି ଫାଇଲ}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{#ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{#ଟି ଆଇଟମ ସେୟାର କରାଯାଉଛି}other{#ଟି ଆଇଟମ ସେୟାର କରାଯାଉଛି}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ଟେକ୍ସଟରେ ଇମେଜ ସେୟାର ହେଉଛି"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ଲିଙ୍କରେ ଇମେଜ ସେୟାର ହେଉଛି"</string> -    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ଏହାକୁ ସେୟାର୍ କରିବା ପାଇଁ କୌଣସି ସୁପାରିଶ କରାଯାଇଥିବା ଲୋକ ନାହାଁନ୍ତି"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ଆପ୍ସ ତାଲିକା"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ଲିଙ୍କ ସହ ଇମେଜ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଇମେଜ ସେୟାର କରାଯାଉଛି}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ଟେକ୍ସଟ ସହ ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{ଟେକ୍ସଟ ସହ #ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ଇମେଜ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ଭିଡିଓ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ଫାଇଲ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string> +    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ଏହାକୁ ସେୟାର କରିବା ପାଇଁ କୌଣସି ସୁପାରିଶ କରାଯାଇଥିବା ଲୋକ ନାହାଁନ୍ତି"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ଏହି ବିଷୟବସ୍ତୁ ୱାର୍କ ଆପଗୁଡ଼ିକରେ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ଏହି ବିଷୟବସ୍ତୁ ବ୍ୟକ୍ତିଗତ ଆପଗୁଡ଼ିକରେ ସେୟାର୍ କରାଯାଇପାରିବ ନାହିଁ"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ଏହି ବିଷୟବସ୍ତୁ ବ୍ୟକ୍ତିଗତ ଆପଗୁଡ଼ିକରେ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ୱାର୍କ ପ୍ରୋଫାଇଲକୁ ବିରତ କରାଯାଇଛି"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ଚାଲୁ କରିବା ପାଇଁ ଟାପ୍ କରନ୍ତୁ"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ୱାର୍କ ଆପ୍ସକୁ ବିରତ କରାଯାଇଛି"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ପୁଣି ଚାଲୁ କରନ୍ତୁ"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"କୌଣସି ୱାର୍କ ଆପ୍ ନାହିଁ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"କୌଣସି ବ୍ୟକ୍ତିଗତ ଆପ୍ ନାହିଁ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>କୁ ଆପଣଙ୍କ ବ୍ୟକ୍ତିଗତ ପ୍ରୋଫାଇଲରେ ଖୋଲିବେ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ଟେକ୍ସଟକୁ ଅନ୍ତର୍ଭୁକ୍ତ କରନ୍ତୁ"</string>      <string name="exclude_link" msgid="1332778255031992228">"ଲିଙ୍କକୁ ବାଦ ଦିଅନ୍ତୁ"</string>      <string name="include_link" msgid="827855767220339802">"ଲିଙ୍କକୁ ଅନ୍ତର୍ଭୁକ୍ତ କରନ୍ତୁ"</string> +    <string name="pinned" msgid="7623664001331394139">"ପିନ କରାଯାଇଛି"</string>  </resources> diff --git a/java/res/values-pa/strings.xml b/java/res/values-pa/strings.xml index 3076880d..872168d6 100644 --- a/java/res/values-pa/strings.xml +++ b/java/res/values-pa/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ਨੂੰ ਪਿੰਨ ਕਰੋ"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ਨੂੰ ਅਨਪਿੰਨ ਕਰੋ"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ਸੰਪਾਦਨ ਕਰੋ"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ਫ਼ਾਈਲ}one{{file_name} + # ਫ਼ਾਈਲ}other{{file_name} + # ਫ਼ਾਈਲਾਂ}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ਫ਼ਾਈਲ}one{+ # ਫ਼ਾਈਲ}other{+ # ਫ਼ਾਈਲਾਂ}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"ਲਿਖਤ ਸੁਨੇਹਾ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ਹੋਰ ਫ਼ਾਈਲ}one{+ # ਹੋਰ ਫ਼ਾਈਲ}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{# ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{# ਚਿੱਤਰ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{# ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{# ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ਆਈਟਮ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}one{# ਆਈਟਮ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}other{# ਆਈਟਮਾਂ ਸਾਂਝੀਆਂ ਕੀਤੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"ਲਿੰਕ ਨਾਲ ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string> -    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ਸਾਂਝਾ ਕਰਨ ਲਈ ਕੋਈ ਸਿਫ਼ਾਰਸ਼ ਕੀਤੇ ਲੋਕ ਨਹੀਂ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ਐਪ ਸੂਚੀ"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ਫ਼ਾਈਲ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}one{# ਫ਼ਾਈਲ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}other{# ਫ਼ਾਈਲਾਂ ਸਾਂਝੀਆਂ ਕੀਤੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਚਿੱਤਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਚਿੱਤਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ਚਿੱਤਰ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ਵੀਡੀਓ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ਫ਼ਾਈਲ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string> +    <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ਸਾਂਝਾ ਕਰਨ ਲਈ ਕੋਈ ਸਿਫ਼ਾਰਸ਼ ਕੀਤਾ ਵਿਅਕਤੀ ਨਹੀਂ ਹੈ"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨਾਲ ਨਹੀਂ ਖੋਲ੍ਹਿਆ ਜਾ ਸਕਦਾ"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਨਿੱਜੀ ਐਪਾਂ ਨਾਲ ਸਾਂਝਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਨਿੱਜੀ ਐਪਾਂ ਨਾਲ ਨਹੀਂ ਖੋਲ੍ਹਿਆ ਜਾ ਸਕਦਾ"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ਕਾਰਜ ਪ੍ਰੋਫਾਈਲ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ਚਾਲੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ਰੋਕ ਹਟਾਓ"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ਕੋਈ ਕੰਮ ਸੰਬੰਧੀ ਐਪ ਨਹੀਂ"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ਕੋਈ ਨਿੱਜੀ ਐਪ ਨਹੀਂ"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ਕੀ ਆਪਣੇ ਨਿੱਜੀ ਪ੍ਰੋਫਾਈਲ ਵਿੱਚ <xliff:g id="APP">%s</xliff:g> ਨੂੰ ਖੋਲ੍ਹਣਾ ਹੈ?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ਲਿਖਤ ਨੂੰ ਸ਼ਾਮਲ ਕਰੋ"</string>      <string name="exclude_link" msgid="1332778255031992228">"ਲਿੰਕ ਨੂੰ ਸ਼ਾਮਲ ਨਾ ਕਰੋ"</string>      <string name="include_link" msgid="827855767220339802">"ਲਿੰਕ ਸ਼ਾਮਲ ਕਰੋ"</string> +    <string name="pinned" msgid="7623664001331394139">"ਪਿੰਨ ਕੀਤਾ ਗਿਆ"</string>  </resources> diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml index 634a32d4..40fe5860 100644 --- a/java/res/values-pl/strings.xml +++ b/java/res/values-pl/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Przypnij aplikację <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Odepnij: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Edytuj"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # plik}few{{file_name} + # pliki}many{{file_name} + # plików}other{{file_name} + # pliku}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i jeszcze # plik}few{i jeszcze # pliki}many{i jeszcze # plików}other{i jeszcze # pliku}}"</string> +    <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_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_items" msgid="5266543892527310331">"{count,plural, =1{Udostępnianie # elementu}few{Udostępnianie # elementów}many{Udostępnianie # elementów}other{Udostępnianie # elementu}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Udostępnianie obrazu z tekstem"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Udostępnianie obrazu z linkiem"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Udostępnianie obrazu przez link}few{Udostępnianie # obrazów przez link}many{Udostępnianie # obrazów przez link}other{Udostępnianie # obrazu przez link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Udostępnianie filmu przez SMS}few{Udostępnianie # filmów przez SMS}many{Udostępnianie # filmów przez SMS}other{Udostępnianie # filmu przez SMS}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura podglądu obrazu"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura podglądu filmu"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura podglądu pliku"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Brak polecanych osób, którym możesz udostępniać"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacji"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tych treści nie można otworzyć w aplikacjach służbowych"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tych treści nie można udostępniać w aplikacjach osobistych"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tych treści nie można otworzyć w aplikacjach osobistych"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Działanie profilu służbowego jest wstrzymane"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Kliknij, aby włączyć"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikacje służbowe są wstrzymane"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Cofnij wstrzymanie"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Brak aplikacji służbowych"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Brak aplikacji osobistych"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otworzyć aplikację <xliff:g id="APP">%s</xliff:g> w profilu osobistym?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Dołącz tekst"</string>      <string name="exclude_link" msgid="1332778255031992228">"Wyklucz link"</string>      <string name="include_link" msgid="827855767220339802">"Dołącz link"</string> +    <string name="pinned" msgid="7623664001331394139">"Przypięte"</string>  </resources> diff --git a/java/res/values-pt-rBR/strings.xml b/java/res/values-pt-rBR/strings.xml index 8f1746fe..ec52fd28 100644 --- a/java/res/values-pt-rBR/strings.xml +++ b/java/res/values-pt-rBR/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Liberar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # arquivo}one{{file_name} + # arquivo}many{{file_name} + # arquivos}other{{file_name} + # arquivos}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string> +    <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_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_items" msgid="5266543892527310331">"{count,plural, =1{Compartilhando # item}one{Compartilhando # item}many{Compartilhando # de itens}other{Compartilhando # itens}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartilhando imagem com texto"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartilhando imagem com link"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartilhando imagem com link}one{Compartilhando # imagem com link}many{Compartilhando # de imagens com link}other{Compartilhando # imagens com link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartilhando vídeo com texto}one{Compartilhando # vídeo com texto}many{Compartilhando # de vídeos com texto}other{Compartilhando # vídeos com texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura da prévia da imagem"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura da prévia do vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura da prévia do arquivo"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não há sugestões de pessoas para compartilhar"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível compartilhar esse conteúdo com apps pessoais"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir esse conteúdo com apps pessoais"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de trabalho está pausado"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toque para ativar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Os apps de trabalho foram pausados"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reativar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nenhum app de trabalho"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nenhum app pessoal"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Incluir texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>      <string name="include_link" msgid="827855767220339802">"Incluir link"</string> +    <string name="pinned" msgid="7623664001331394139">"Fixada"</string>  </resources> diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml index cc2bd472..c60b923b 100644 --- a/java/res/values-pt-rPT/strings.xml +++ b/java/res/values-pt-rPT/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Soltar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ficheiro}many{{file_name} + # ficheiros}other{{file_name} + # ficheiros}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ficheiro}many{+ # ficheiros}other{+ # ficheiros}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"A partilhar texto"</string> -    <string name="sharing_link" msgid="2307694372813942916">"A partilhar link"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{A partilhar imagem}many{A partilhar # imagens}other{A partilhar # imagens}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{E mais # ficheiro}many{E mais # ficheiros}other{E mais # ficheiros}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Partilhar texto"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Partilhar link"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partilhar imagem}many{Partilhar # imagens}other{Partilhar # imagens}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{A partilhar vídeo}many{A partilhar # vídeos}other{A partilhar # vídeos}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{A partilhar # item}many{A partilhar # itens}other{A partilhar # itens}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"A partilh. imag. c/ texto"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"A partilhar imag. c/ link"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{A partilhar # ficheiro}many{A partilhar # ficheiros}other{A partilhar # ficheiros}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{A partilhar imagem com texto}many{A partilhar # imagens com texto}other{A partilhar # imagens com texto}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{A partilhar imagem com link}many{A partilhar # imagens com link}other{A partilhar # imagens com link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{A partilhar vídeo com texto}many{A partilhar # vídeos com texto}other{A partilhar # vídeos com texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de pré-visualização da imagem"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de pré-visualização do vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de pré-visualização do ficheiro"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não existem pessoas recomendadas com quem partilhar"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicações"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir este conteúdo com apps de trabalho"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível partilhar este conteúdo com apps pessoais"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir este conteúdo com apps pessoais"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Perfil de trabalho em pausa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocar para ativar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"As apps de trabalho estão pausadas"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Retomar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Sem apps de trabalho"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Sem apps pessoais"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir a app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Incluir texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>      <string name="include_link" msgid="827855767220339802">"Incluir link"</string> +    <string name="pinned" msgid="7623664001331394139">"Afixada"</string>  </resources> diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml index 8f1746fe..ec52fd28 100644 --- a/java/res/values-pt/strings.xml +++ b/java/res/values-pt/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Liberar <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # arquivo}one{{file_name} + # arquivo}many{{file_name} + # arquivos}other{{file_name} + # arquivos}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string> +    <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_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_items" msgid="5266543892527310331">"{count,plural, =1{Compartilhando # item}one{Compartilhando # item}many{Compartilhando # de itens}other{Compartilhando # itens}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartilhando imagem com texto"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartilhando imagem com link"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartilhando imagem com link}one{Compartilhando # imagem com link}many{Compartilhando # de imagens com link}other{Compartilhando # imagens com link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartilhando vídeo com texto}one{Compartilhando # vídeo com texto}many{Compartilhando # de vídeos com texto}other{Compartilhando # vídeos com texto}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura da prévia da imagem"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura da prévia do vídeo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura da prévia do arquivo"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não há sugestões de pessoas para compartilhar"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível compartilhar esse conteúdo com apps pessoais"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir esse conteúdo com apps pessoais"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de trabalho está pausado"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toque para ativar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Os apps de trabalho foram pausados"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reativar"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nenhum app de trabalho"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nenhum app pessoal"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Incluir texto"</string>      <string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>      <string name="include_link" msgid="827855767220339802">"Incluir link"</string> +    <string name="pinned" msgid="7623664001331394139">"Fixada"</string>  </resources> diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml index 72962442..d6cae158 100644 --- a/java/res/values-ro/strings.xml +++ b/java/res/values-ro/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fixează <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Anulează fixarea pentru <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Editează"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fișier}few{{file_name} + # fișiere}other{{file_name} + # de fișiere}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fișier}few{+ # fișiere}other{+ # de fișiere}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Încă un fișier}few{Încă # fișiere}other{Încă # de fișiere}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Se trimite textul"</string>      <string name="sharing_link" msgid="2307694372813942916">"Se trimite linkul"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Se trimite imaginea}few{Se trimit # imagini}other{Se trimit # de imagini}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Se trimite videoclipul}few{Se trimit # videoclipuri}other{Se trimit # de videoclipuri}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Se trimite # element}few{Se trimit # elemente}other{Se trimit # de elemente}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Se trimite imaginea cu text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Se trimite imaginea cu linkul"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Se trimite un fișier}few{Se trimit # fișiere}other{Se trimit # de fișiere}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Se trimite imaginea cu text}few{Se trimit # imagini cu text}other{Se trimit # de imagini cu text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Se trimite imaginea cu linkul}few{Se trimit # imagini cu linkul}other{Se trimit # de imagini cu linkul}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Se trimite videoclipul cu text}few{Se trimit # videoclipuri cu text}other{Se trimit # de videoclipuri cu text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatură pentru previzualizarea imaginii"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatură pentru previzualizarea videoclipului"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatură pentru previzualizarea fișierului"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nu există persoane recomandate pentru permiterea accesului"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicații"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Acest conținut nu poate fi trimis cu aplicații personale"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Acest conținut nu poate fi trimis către aplicații personale"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Acest conținut nu poate fi deschis cu aplicații personale"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profilul de serviciu este întrerupt"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Atinge pentru a activa"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplicațiile pentru lucru sunt întrerupte"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivează"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nicio aplicație pentru lucru"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nicio aplicație personală"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Deschizi <xliff:g id="APP">%s</xliff:g> în profilul personal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Include textul"</string>      <string name="exclude_link" msgid="1332778255031992228">"Exclude linkul"</string>      <string name="include_link" msgid="827855767220339802">"Include linkul"</string> +    <string name="pinned" msgid="7623664001331394139">"Fixat"</string>  </resources> diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml index 2db2c5ea..618e0a6f 100644 --- a/java/res/values-ru/strings.xml +++ b/java/res/values-ru/strings.xml @@ -53,29 +53,38 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Закрепить приложение \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Открепить приложение \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Изменить"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{\"{file_name}\" и ещё # файл}one{\"{file_name}\" и ещё # файл}few{\"{file_name}\" и ещё # файла}many{\"{file_name}\" и ещё # файлов}other{\"{file_name}\" и ещё # файла}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{и ещё # файл}one{и ещё # файл}few{и ещё # файла}many{и ещё # файлов}other{и ещё # файла}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ ещё # файл}one{+ ещё # файл}few{+ ещё # файла}many{+ ещё # файлов}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{Отправка # изображений}many{Отправка # изображений}other{Отправка # изображения}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Отправка видео}one{Отправка # видео}few{Отправка # видео}many{Отправка # видео}other{Отправка # видео}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Отправка # объекта}one{Отправка # объекта}few{Отправка # объектов}many{Отправка # объектов}other{Отправка # объекта}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Сообщение с изображением"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Ссылка на изображение"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Предоставляется доступ к # файлу}one{Предоставляется доступ к # файлу}few{Предоставляется доступ к # файлам}many{Предоставляется доступ к # файлам}other{Предоставляется доступ к # файла}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Отправка изображения с текстом}one{Отправка # изображения с текстом}few{Отправка # изображений с текстом}many{Отправка # изображений с текстом}other{Отправка # изображения с текстом}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Отправка изображения со ссылкой}one{Отправка # изображения со ссылкой}few{Отправка # изображений со ссылкой}many{Отправка # изображений со ссылкой}other{Отправка # изображения со ссылкой}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Отправка видео с текстом}one{Отправка # видео с текстом}few{Отправка # видео с текстом}many{Отправка # видео с текстом}other{Отправка # видео с текстом}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Значок предварительного просмотра изображения"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Значок предварительного просмотра видео"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Значок предварительного просмотра файла"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Рекомендованных получателей нет."</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список приложений"</string>      <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_personal_tab_accessibility" msgid="4467784352232582574">"Просмотр личных данных"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Просмотр рабочих данных"</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_share_with_work_apps_explanation" msgid="2984105853145456723">"Этим контентом нельзя делиться с рабочими приложениями."</string>      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Этот контент нельзя открыть в рабочем приложении."</string> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Этот контент нельзя открывать через личные приложения."</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Этим контентом нельзя делиться с личными приложениями."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Этот контент нельзя открыть в личном приложении."</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Действие рабочего профиля приостановлено."</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Нажмите, чтобы включить"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Рабочие приложения приостановлены"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Включить"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Не поддерживается рабочими приложениями."</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Не поддерживается личными приложениями."</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Открыть приложение \"<xliff:g id="APP">%s</xliff:g>\" в личном профиле?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Вернуть текст"</string>      <string name="exclude_link" msgid="1332778255031992228">"Исключить ссылку"</string>      <string name="include_link" msgid="827855767220339802">"Вернуть ссылку"</string> +    <string name="pinned" msgid="7623664001331394139">"Закреплено"</string>  </resources> diff --git a/java/res/values-si/strings.xml b/java/res/values-si/strings.xml index bbb01071..176206e8 100644 --- a/java/res/values-si/strings.xml +++ b/java/res/values-si/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> අමුණන්න"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ඇමුණුම ඉවත් කරන්න"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"සංස්කරණය"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ගොනු #}one{{file_name} + ගොනු #}other{{file_name} + ගොනු #}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ගොනුවක්}one{ගොනු + #}other{ගොනු + #}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{තව + # ගොනුවක්}one{තව ගොනු + #}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{රූප #ක් බෙදා ගැනීම}other{රූප #ක් බෙදා ගැනීම}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{වීඩියෝව බෙදා ගැනීම}one{වීඩියෝ #ක් බෙදා ගැනීම}other{වීඩියෝ #ක් බෙදා ගැනීම}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# අයිතමයක් බෙදා ගැනීම}one{අයිතම #ක් බෙදා ගැනීම}other{අයිතම #ක් බෙදා ගැනීම}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"පෙළ සමග රූපය බෙදා ගැනීම"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"සබැඳිය සමග රූපය බෙදාගැනීම"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ගොනුවක් බෙදා ගැනීම}one{ගොනු #ක් බෙදා ගැනීම}other{ගොනු #ක් බෙදා ගැනීම}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{පෙළ සමග රූපය බෙදා ගැනීම}one{පෙළ සමග රූප #ක් බෙදා ගැනීම}other{පෙළ සමග රූප #ක් බෙදා ගැනීම}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{සබැඳිය සමග රූපය බෙදා ගැනීම}one{සබැඳිය සමග රූප #ක් බෙදා ගැනීම}other{සබැඳිය සමග රූප #ක් බෙදා ගැනීම}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{පෙළ සමග වීඩියෝව බෙදා ගැනීම}one{පෙළ සමග වීඩියෝ #ක් බෙදා ගැනීම}other{පෙළ සමග වීඩියෝ #ක් බෙදා ගැනීම}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"රූප පෙරදසුන් සිඟිති රුව"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"වීඩියෝ පෙරදසුන් සිඟිති රුව"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ගොනු පෙරදසුන් සිඟිති රුව"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"බෙදා ගැනීමට නිර්දේශිත පුද්ගලයන් නැත"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"යෙදුම් ලැයිස්තුව"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"මෙම අන්තර්ගතය කාර්යාල යෙදුම් සමඟ විවෘත කළ නොහැකිය"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"මෙම අන්තර්ගතය පුද්ගලික යෙදුම් සමඟ බෙදා ගත නොහැකිය"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"මෙම අන්තර්ගතය පුද්ගලික යෙදුම් සමඟ විවෘත කළ නොහැකිය"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"කාර්යාල පැතිකඩ විරාම කර ඇත"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ක්රියාත්මක කිරීමට තට්ටු කරන්න"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"කාර්යාල යෙදුම් විරාම කර ඇත"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"විරාම නොකරන්න"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"කාර්යාල යෙදුම් නැත"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"පුද්ගලික යෙදුම් නැත"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ඔබගේ පුද්ගලික පැතිකඩ තුළ විවෘත කරන්නද?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"පාඨය ඇතළත් කරන්න"</string>      <string name="exclude_link" msgid="1332778255031992228">"සබැඳිය බැහැර කරන්න"</string>      <string name="include_link" msgid="827855767220339802">"සබැඳිය ඇතුළත් කරන්න"</string> +    <string name="pinned" msgid="7623664001331394139">"අමුණා ඇත"</string>  </resources> diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml index 7e96d4ad..1ac43e60 100644 --- a/java/res/values-sk/strings.xml +++ b/java/res/values-sk/strings.xml @@ -53,20 +53,29 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pripnúť aplikáciu <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Odopnúť <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Upraviť"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # súbor}few{{file_name} + # súbory}many{{file_name} + # files}other{{file_name} + # súborov}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # súbor}few{+ # súbory}many{+ # files}other{+ # súborov}}"</string> +    <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_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_items" msgid="5266543892527310331">"{count,plural, =1{Zdieľa sa # položka}few{Zdieľajú sa # položky}many{Sharing # items}other{Zdieľa sa # položiek}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Zdieľa sa obr. s textom"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Zdieľa sa obr. s odkazom"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Zdieľa sa obrázok s odkazom}few{Zdieľajú sa # obrázky s odkazom}many{Sharing # images with link}other{Zdieľa sa # obrázkov s odkazom}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Zdieľa sa video s textom}few{Zdieľajú sa # videá s textom}many{Sharing # videos with text}other{Zdieľa sa # videí s textom}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatúra ukážky obrázka"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatúra ukážky videa"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatúra ukážky súboru"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Žiadni odporúčaní príjemcovia"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Zoznam aplikácií"</string>      <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">"Práca"</string> +    <string name="resolver_work_tab" msgid="3588325717455216412">"Pracovné"</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_cross_profile_blocked" msgid="3515194063758605377">"Blokované vaším správcom IT"</string> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah sa nedá otvoriť pomocou pracovných aplikácií"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tento obsah sa nedá zdieľať pomocou osobných aplikácií"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tento obsah sa nedá otvoriť pomocou osobných aplikácií"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Pracovný profil je pozastavený"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Zapnúť klepnutím"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pracovné aplikácie sú pozastavené"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Zrušiť pozastavenie"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Žiadne pracovné aplikácie"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Žiadne osobné aplikácie"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Chcete otvoriť <xliff:g id="APP">%s</xliff:g> v osobnom profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Zahrnúť text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Vylúčiť odkaz"</string>      <string name="include_link" msgid="827855767220339802">"Zahrnúť odkaz"</string> +    <string name="pinned" msgid="7623664001331394139">"Pripnuté"</string>  </resources> diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml index b2aabdd0..0ef88727 100644 --- a/java/res/values-sl/strings.xml +++ b/java/res/values-sl/strings.xml @@ -53,20 +53,29 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Pripni aplikacijo <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Odpni aplikacijo <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # datoteka}one{{file_name} + # datoteka}two{{file_name} + # datoteki}few{{file_name} + # datoteke}other{{file_name} + # datotek}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # datoteka}one{+ # datoteka}two{+ # datoteki}few{+ # datoteke}other{+ # datotek}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ še # datoteka}one{+ še # datoteka}two{+ še # datoteki}few{+ še # datoteke}other{+ še # datotek}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Deljenje besedila"</string>      <string name="sharing_link" msgid="2307694372813942916">"Deljenje povezave"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deljenje slike}one{Deljenje # slike}two{Deljenje # slik}few{Deljenje # slik}other{Deljenje # slik}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deljenje videoposnetka}one{Deljenje # videoposnetka}two{Deljenje # videoposnetkov}few{Deljenje # videoposnetkov}other{Deljenje # videoposnetkov}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deljenje # elementa}one{Deljenje # elementa}two{Deljenje # elementov}few{Deljenje # elementov}other{Deljenje # elementov}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Deljenje slike z besedilom"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Deljenje slike s povezavo"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deljenje # datoteke}one{Deljenje # datoteke}two{Deljenje # datotek}few{Deljenje # datotek}other{Deljenje # datotek}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deljenje slike z besedilom}one{Deljenje # slike z besedilom}two{Deljenje # slik z besedilom}few{Deljenje # slik z besedilom}other{Deljenje # slik z besedilom}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deljenje slike s povezavo}one{Deljenje # slike s povezavo}two{Deljenje # slik s povezavo}few{Deljenje # slik s povezavo}other{Deljenje # slik s povezavo}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deljenje videoposnetka z besedilom}one{Deljenje # videoposnetka z besedilom}two{Deljenje # videoposnetkov z besedilom}few{Deljenje # videoposnetkov z besedilom}other{Deljenje # videoposnetkov z besedilom}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica predogleda slike"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica predogleda videoposnetka"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica predogleda datoteke"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ni priporočenih oseb za deljenje vsebine."</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Seznam aplikacij"</string>      <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">"Služba"</string> +    <string name="resolver_work_tab" msgid="3588325717455216412">"Delo"</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_cross_profile_blocked" msgid="3515194063758605377">"Blokiral skrbnik za IT"</string> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Te vsebine ni mogoče odpreti z delovnimi aplikacijami."</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Te vsebine ni mogoče deliti z osebnimi aplikacijami."</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Te vsebine ni mogoče odpreti z osebnimi aplikacijami."</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Delovni profil je začasno zaustavljen"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dotaknite se za vklop"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Delovne aplikacije so začasno zaustavljene"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Znova aktiviraj"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nobena delovna aplikacija ni na voljo"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nobena osebna aplikacija"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite aplikacijo <xliff:g id="APP">%s</xliff:g> odpreti v osebnem profilu?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Vključi besedilo"</string>      <string name="exclude_link" msgid="1332778255031992228">"Izloči povezavo"</string>      <string name="include_link" msgid="827855767220339802">"Vključi povezavo"</string> +    <string name="pinned" msgid="7623664001331394139">"Pripeto"</string>  </resources> diff --git a/java/res/values-sq/strings.xml b/java/res/values-sq/strings.xml index 37fb755f..95c3e57c 100644 --- a/java/res/values-sq/strings.xml +++ b/java/res/values-sq/strings.xml @@ -31,7 +31,7 @@      <string name="whichEditApplicationNamed" msgid="3150137489226219100">"Modifiko me <xliff:g id="APP">%1$s</xliff:g>"</string>      <string name="whichEditApplicationLabel" msgid="5992662938338600364">"Redakto"</string>      <string name="whichSendApplication" msgid="59510564281035884">"Ndaj"</string> -    <string name="whichSendApplicationNamed" msgid="495577664218765855">"Shpërndaj me <xliff:g id="APP">%1$s</xliff:g>"</string> +    <string name="whichSendApplicationNamed" msgid="495577664218765855">"Ndaj me <xliff:g id="APP">%1$s</xliff:g>"</string>      <string name="whichSendApplicationLabel" msgid="2391198069286568035">"Ndaj"</string>      <string name="whichSendToApplication" msgid="2724450540348806267">"Dërgo me"</string>      <string name="whichSendToApplicationNamed" msgid="1996548940365954543">"Dërgo duke përdorur <xliff:g id="APP">%1$s</xliff:g>"</string> @@ -53,29 +53,38 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Gozhdo \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Zhgozhdoje <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Modifiko"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # skedar}other{{file_name} + # skedarë}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # skedar}other{+ # skedarë}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # skedar tjetër}other{+ # skedarë të tjerë}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Po ndahet teksti"</string>      <string name="sharing_link" msgid="2307694372813942916">"Po ndahet lidhja"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Po ndahet imazh}other{Po ndahen # imazhe}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Po ndahet videoja}other{Po ndahen # video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Po ndahet # artikull}other{Po ndahen # artikuj}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Po ndahet imazh me tekst"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Po ndahet imazh me lidhje"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Po ndahet # skedar}other{Po ndahen # skedarë}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Po ndahet një imazh me tekst}other{Po ndahen # imazhe me tekst}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Po ndahet një imazh me lidhje}other{Po ndahen # imazhe me lidhje}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Po ndahet një video me tekst}other{Po ndahen # video me tekst}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura e pamjes paraprake të imazhit"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura e pamjes paraprake të videos"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura e pamjes paraprake të skedarit"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nuk ka persona të rekomanduar për ta ndarë"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista e aplikacioneve"</string>      <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_personal_tab_accessibility" msgid="4467784352232582574">"Pamja personale"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pamja e punës"</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ë shpërndahet me aplikacione pune"</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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kjo përmbajtje nuk mund të shpërndahet me aplikacione personale"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kjo përmbajtje nuk mund të ndahet me aplikacione personale"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Kjo përmbajtje nuk mund të hapet me aplikacione personale"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profili i punës është në pauzë"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Trokit për ta aktivizuar"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikacionet e punës janë vendosur në pauzë"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hiq nga pauza"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nuk ka aplikacione pune"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nuk ka aplikacione personale"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Të hapet <xliff:g id="APP">%s</xliff:g> në profilin tënd personal?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Përfshi tekstin"</string>      <string name="exclude_link" msgid="1332778255031992228">"Përjashto lidhjen"</string>      <string name="include_link" msgid="827855767220339802">"Përfshi lidhjen"</string> +    <string name="pinned" msgid="7623664001331394139">"U gozhdua"</string>  </resources> diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml index fb881642..511a1293 100644 --- a/java/res/values-sr/strings.xml +++ b/java/res/values-sr/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Закачите особу <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Откачи апликацију <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Измени"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # фајл}one{{file_name} + # фајл}few{{file_name} + # фајла}other{{file_name} + # фајлова}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{и још # фајл}one{и још # фајл}few{и још # фајла}other{и још # фајлова}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Дели се видео}one{Дели се # видео}few{Деле се # видео снимка}other{Дели се # видео снимака}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Дели се # ставка}one{Дели се # ставка}few{Деле се # ставке}other{Дели се # ставки}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Дели се слика са текстом"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Дели се слика са линком"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Дели се слика са линком}one{Дели се # слика са линком}few{Деле се # слике са линком}other{Дели се # слика са линком}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Дели се видео са текстом}one{Дели се # видео са текстом}few{Деле се # видео снимка са текстом}other{Дели се # видеа са текстом}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Сличица за преглед слике"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Сличица за преглед видеа"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Сличица за преглед фајла"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Нема препоручених људи за дељење"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Листа апликација"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овај садржај не може да се отвара помоћу пословних апликација"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Овај садржај не може да се дели помоћу личних апликација"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Овај садржај не може да се отвара помоћу личних апликација"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Пословни профил је паузиран"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Додирните да бисте укључили"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Пословне апликације су паузиране"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Поново активирај"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Нема пословних апликација"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Нема личних апликација"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Желите да на личном профилу отворите: <xliff:g id="APP">%s</xliff:g>?"</string> @@ -84,6 +93,7 @@      <string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Користи пословни прегледач"</string>      <string name="exclude_text" msgid="5508128757025928034">"Искључи текст"</string>      <string name="include_text" msgid="642280283268536140">"Уврсти текст"</string> -    <string name="exclude_link" msgid="1332778255031992228">"Искључи линк"</string> +    <string name="exclude_link" msgid="1332778255031992228">"Изузми линк"</string>      <string name="include_link" msgid="827855767220339802">"Уврсти линк"</string> +    <string name="pinned" msgid="7623664001331394139">"Закачено"</string>  </resources> diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml index 37c7f685..7ed2d3f1 100644 --- a/java/res/values-sv/strings.xml +++ b/java/res/values-sv/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Fäst <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Lossa <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Redigera"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}other{{file_name} + # filer}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}other{+ # filer}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{och # fil till}other{och # filer till}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Delar text"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Delar länk"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Delningslänk"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Delar bild}other{Delar # bilder}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Delar video}other{Delar # videor}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Delar # objekt}other{Delar # objekt}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Delar bild med text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Delar bild med länk"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Delar # fil}other{Delar # filer}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Delar bild med text}other{Delar # bilder med text}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Delar bild med länk}other{Delar # bilder med länk}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Delar video med text}other{Delar # videor med text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatyr av förhandsgranskning av bild"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatyr av förhandsgranskning av video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatyr av förhandsgranskning av fil"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Inga rekommenderade personer att dela med"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Applista"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Det här innehållet kan inte öppnas med jobbappar"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Det här innehållet kan inte delas med privata appar"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Det här innehållet kan inte öppnas med privata appar"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Jobbprofilen är pausad"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tryck för att aktivera"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Jobbappar har pausats"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Återuppta"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Inga jobbappar"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Inga privata appar"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vill du öppna <xliff:g id="APP">%s</xliff:g> i din privata profil?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Inkludera text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Uteslut länk"</string>      <string name="include_link" msgid="827855767220339802">"Inkludera länk"</string> +    <string name="pinned" msgid="7623664001331394139">"Fäst"</string>  </resources> diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml index f8aa1ea3..de45a78c 100644 --- a/java/res/values-sw/strings.xml +++ b/java/res/values-sw/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Bandika <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Bandua <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Badilisha"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + faili #}other{{file_name} + faili #}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ faili #}other{+ faili #}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Faili nyingine #}other{Faili zingine #}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Inashiriki maandishi"</string>      <string name="sharing_link" msgid="2307694372813942916">"Inashiriki kiungo"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Inashiriki picha}other{Inashiriki picha #}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Inashiriki video}other{Inashiriki video #}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Inashiriki kipengee #}other{Inashiriki vipengee #}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Inashiriki picha na maandishi"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Inashiriki picha na kiungo"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Inashiriki faili #}other{Inashiriki faili #}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Inashiriki picha na maandishi}other{Inashiriki picha # na maandishi}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Inashiriki picha na kiungo}other{Inashiriki picha # na kiungo}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Inashiriki video na maandishi}other{Inashiriki video # na maandishi}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Kijipicha cha onyesho la kukagua picha"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Kijipicha cha onyesho la kukagua video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Kijipicha cha onyesho la kukagua faili"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Hujapendekezewa watu wa kushiriki nao"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Orodha ya programu"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Huwezi kufungua maudhui haya ukitumia programu za kazini"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Huwezi kushiriki maudhui haya na programu za binafsi"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Huwezi kufungua maudhui haya ukitumia programu za binafsi"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Wasifu wa kazini umesimamishwa"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Gusa ili uwashe"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Programu za kazini zimesitishwa"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Acha kusitisha"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Hakuna programu za kazini"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Hakuna programu za binafsi"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Je, unataka kufungua <xliff:g id="APP">%s</xliff:g> katika wasifu wako binafsi?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Jumuisha maandishi"</string>      <string name="exclude_link" msgid="1332778255031992228">"Usijumuishe kiungo"</string>      <string name="include_link" msgid="827855767220339802">"Jumuisha kiungo"</string> +    <string name="pinned" msgid="7623664001331394139">"Imebandikwa"</string>  </resources> diff --git a/java/res/values-ta/strings.xml b/java/res/values-ta/strings.xml index da13e7d1..c95e5cb1 100644 --- a/java/res/values-ta/strings.xml +++ b/java/res/values-ta/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ஆப்ஸைப் பின் செய்"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ஐப் பின் நீக்கு"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"திருத்து"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ஃபைல்}other{{file_name} + # ஃபைல்கள்}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ஃபைல்}other{+ # ஃபைல்கள்}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"உரையைப் பகிர்கிறது"</string> -    <string name="sharing_link" msgid="2307694372813942916">"பகிர்வதற்கான இணைப்பு"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{வீடியோவைப் பகிர்கிறது}other{# வீடியோக்களை பகிர்கிறது}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ஃபைலைப் பகிர்கிறது}other{# ஃபைல்களைப் பகிர்கிறது}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"உரையுடன் படம் பகிர்தல்"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"இணைப்புடன் படம் பகிர்தல்"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{இணைப்பைக் கொண்ட படத்தைப் பகிர்கிறது}other{இணைப்பைக் கொண்ட # படங்களைப் பகிர்கிறது}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{வார்த்தைகளைக் கொண்ட வீடியோவைப் பகிர்கிறது}other{வார்த்தைகளைக் கொண்ட # வீடியோக்களைப் பகிர்கிறது}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"படத்தின் மாதிரிக்காட்சிச் சிறுபடம்"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"வீடியோவின் மாதிரிக்காட்சிச் சிறுபடம்"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ஃபைலின் மாதிரிக்காட்சிச் சிறுபடம்"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"பகிர்வதற்கு எவரும் பரிந்துரைக்கப்படவில்லை"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ஆப்ஸ் பட்டியல்"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"பணி ஆப்ஸ் மூலம் இந்த உள்ளடக்கத்தைத் திறக்க முடியாது"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"தனிப்பட்ட ஆப்ஸுடன் இந்த உள்ளடக்கத்தைப் பகிர முடியாது"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"தனிப்பட்ட ஆப்ஸ் மூலம் இந்த உள்ளடக்கத்தைத் திறக்க முடியாது"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"பணிக் கணக்கு இடைநிறுத்தப்பட்டுள்ளது"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ஆன் செய்யத் தட்டுக"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"பணி ஆப்ஸ் இடைநிறுத்தப்பட்டுள்ளன"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"மீண்டும் இயக்கு"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"பணி ஆப்ஸ் எதுவுமில்லை"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"தனிப்பட்ட ஆப்ஸ் எதுவுமில்லை"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"உங்கள் தனிப்பட்ட கணக்கில் <xliff:g id="APP">%s</xliff:g> ஆப்ஸைத் திறக்கவா?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"வார்த்தைகளைச் சேர்"</string>      <string name="exclude_link" msgid="1332778255031992228">"இணைப்பைத் தவிர்"</string>      <string name="include_link" msgid="827855767220339802">"இணைப்பைச் சேர்"</string> +    <string name="pinned" msgid="7623664001331394139">"பின் செய்யப்பட்டுள்ளது"</string>  </resources> diff --git a/java/res/values-te/strings.xml b/java/res/values-te/strings.xml index 7f430eb7..a8b9457a 100644 --- a/java/res/values-te/strings.xml +++ b/java/res/values-te/strings.xml @@ -35,7 +35,7 @@      <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> +    <string name="whichSendToApplicationLabel" msgid="6909037198280591110">"పంపండి"</string>      <string name="whichHomeApplication" msgid="8797832422254564739">"హోమ్ యాప్ను ఎంచుకోండి"</string>      <string name="whichHomeApplicationNamed" msgid="3943122502791761387">"<xliff:g id="APP">%1$s</xliff:g> యాప్ను హోమ్ పేజీగా ఉపయోగించండి"</string>      <string name="whichHomeApplicationLabel" msgid="2066319585322981524">"చిత్రాన్ని క్యాప్చర్ చేయి"</string> @@ -53,29 +53,38 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>ను పిన్ చేయండి"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ను అన్పిన్ చేయి"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ఎడిట్ చేయండి"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ఫైల్}other{{file_name} + # ఫైల్స్}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ఫైల్}other{+ # ఫైల్స్}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{వీడియోను షేర్ చేయడం}other{# వీడియోలను షేర్ చేయడం}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ఐటెమ్ను షేర్ చేయడం}other{# ఐటెమ్లను షేర్ చేయడం}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"టెక్స్ట్తో ఇమేజ్ను షేర్ చేయడం"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"లింక్తో ఇమేజ్ను షేర్ చేయడం"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{లింక్ చేయడం ద్వారా ఇమేజ్ను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # ఇమేజ్లను షేర్ చేయడం}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా వీడియోను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # వీడియోలను షేర్ చేయడం}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ఇమేజ్ ప్రివ్యూ థంబ్నెయిల్"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"వీడియో ప్రివ్యూ థంబ్నెయిల్"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ఫైల్ ప్రివ్యూ థంబ్నెయిల్"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ఎవరికి షేర్ చేయాలనే దానికి సంబంధించి సిఫార్సులేవీ లేవు"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"యాప్ల లిస్ట్"</string>      <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_work_tab" msgid="3588325717455216412">"వర్క్ ప్లేస్"</string>      <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"వ్యక్తిగత వీక్షణ"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"పని వీక్షణ"</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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ఈ కంటెంట్ వ్యక్తిగత యాప్తో షేర్ చేయడం సాధ్యం కాదు"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ఈ కంటెంట్ను వ్యక్తిగత యాప్స్ లోకి షేర్ చేయడం సాధ్యం కాదు"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ఈ కంటెంట్ వ్యక్తిగత యాప్తో తెరవడం సాధ్యం కాదు"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"వర్క్ ప్రొఫైల్ పాజ్ చేయబడింది"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"ఆన్ చేయడానికి ట్యాప్ చేయండి"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"వర్క్ యాప్లు పాజ్ చేయబడ్డాయి"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"అన్పాజ్ చేయండి"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"వర్క్ యాప్లు లేవు"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"వ్యక్తిగత యాప్లు లేవు"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>ను మీ వ్యక్తిగత ప్రొఫైల్లో తెరవాలా?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"టెక్స్ట్ను చేర్చండి"</string>      <string name="exclude_link" msgid="1332778255031992228">"లింక్ను మినహాయించండి"</string>      <string name="include_link" msgid="827855767220339802">"లింక్ను చేర్చండి"</string> +    <string name="pinned" msgid="7623664001331394139">"పిన్ చేయబడింది"</string>  </resources> diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml index 70519849..af91064b 100644 --- a/java/res/values-th/strings.xml +++ b/java/res/values-th/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"ปักหมุด <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"เลิกปักหมุด <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"แก้ไข"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ไฟล์}other{{file_name} + # ไฟล์}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{อีก # ไฟล์}other{อีก # ไฟล์}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{กำลังแชร์วิดีโอ}other{กำลังแชร์วิดีโอ # รายการ}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{กำลังแชร์ # รายการ}other{กำลังแชร์ # รายการ}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"กำลังแชร์รูปภาพพร้อมข้อความ"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"กำลังแชร์รูปภาพพร้อมลิงก์"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{กำลังแชร์รูปภาพพร้อมลิงก์}other{กำลังแชร์รูปภาพ # รายการพร้อมลิงก์}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{กำลังแชร์วิดีโอพร้อมข้อความ}other{กำลังแชร์วิดีโอ # รายการพร้อมข้อความ}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"ภาพตัวอย่างขนาดย่อของรูปภาพ"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ภาพตัวอย่างขนาดย่อของวิดีโอ"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"ภาพตัวอย่างขนาดย่อของไฟล์"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ไม่พบใครที่แนะนำให้แชร์ด้วย"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"รายชื่อแอป"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"เปิดเนื้อหานี้โดยใช้แอปงานไม่ได้"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"แชร์เนื้อหานี้โดยใช้แอปส่วนตัวไม่ได้"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"เปิดเนื้อหานี้โดยใช้แอปส่วนตัวไม่ได้"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"โปรไฟล์งานหยุดชั่วคราว"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"แตะเพื่อเปิด"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"แอปงานหยุดชั่วคราว"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"ยกเลิกการหยุดชั่วคราว"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ไม่มีแอปงาน"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ไม่มีแอปส่วนตัว"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"เปิด <xliff:g id="APP">%s</xliff:g> ในโปรไฟล์ส่วนตัวไหม"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"รวมข้อความ"</string>      <string name="exclude_link" msgid="1332778255031992228">"ไม่รวมลิงก์"</string>      <string name="include_link" msgid="827855767220339802">"รวมลิงก์"</string> +    <string name="pinned" msgid="7623664001331394139">"ปักหมุดไว้"</string>  </resources> diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml index b7c50d4b..cb4ff654 100644 --- a/java/res/values-tl/strings.xml +++ b/java/res/values-tl/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"I-pin ang <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"I-unpin ang <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"I-edit"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}one{{file_name} + # file}other{{file_name} + # na file}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}one{+ # file}other{+ # na file}}"</string> +    <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_videos" msgid="3583423190182877434">"{count,plural, =1{Ibinabahagi ang video}one{Ibinabahagi ang # video}other{Ibinabahagi ang # na video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Ibinabahagi ang # item}one{Ibinabahagi ang # item}other{Ibinabahagi ang # na item}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Larawang may text"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Larawang may link"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Nagbabahagi ng larawang may link}one{Nagbabahagi ng # larawang may link}other{Nagbabahagi ng # na larawang may link}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Nagbabahagi ng video na may text}one{Nagbabahagi ng # video na may text}other{Nagbabahagi ng # na video na may text}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Thumbnail ng preview ng larawan"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Thumbnail ng preview ng video"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Thumbnail ng preview ng file"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Walang inirerekomendang taong mapagbabahagian"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Listahan ng mga app"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hindi puwedeng buksan sa mga app para sa trabaho ang content na ito"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hindi puwedeng ibahagi sa mga personal na app ang content na ito"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Hindi puwedeng buksan sa mga personal na app ang content na ito"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Naka-pause ang profile sa trabaho"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"I-tap para i-on"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Naka-pause ang mga app para sa trabaho"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"I-unpause"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Walang app para sa trabaho"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Walang personal na app"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buksan ang <xliff:g id="APP">%s</xliff:g> sa iyong personal na profile?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Isama ang text"</string>      <string name="exclude_link" msgid="1332778255031992228">"Huwag isama ang link"</string>      <string name="include_link" msgid="827855767220339802">"Isama ang link"</string> +    <string name="pinned" msgid="7623664001331394139">"Naka-pin"</string>  </resources> diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml index 71168718..53d74bb9 100644 --- a/java/res/values-tr/strings.xml +++ b/java/res/values-tr/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Şunu sabitle: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> uygulamasının sabitlemesini kaldır"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Düzenle"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # dosya}other{{file_name} + # dosya}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # dosya}other{+ # dosya}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # dosya daha}other{+ # dosya daha}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Metin paylaşılıyor"</string>      <string name="sharing_link" msgid="2307694372813942916">"Bağlantı paylaşılıyor"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Resim paylaşılıyor}other{# resim paylaşılıyor}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video paylaşılıyor}other{# video paylaşılıyor}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# öğe paylaşılıyor}other{# öğe paylaşılıyor}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Metin ekli resim paylaşılıyor"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Bağlantı ekli resim paylaşılıyor"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# dosya paylaşılıyor}other{# dosya paylaşılıyor}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Metin ekli resim paylaşılıyor}other{Metin ekli # resim paylaşılıyor}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bağlantı ekli resim paylaşılıyor}other{Bağlantı ekli # resim paylaşılıyor}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Metin ekli video paylaşılıyor}other{Metin ekli # video paylaşılıyor}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Resim önizleme küçük resmi"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Video önizleme küçük resmi"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Dosya önizleme küçük resmi"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Paylaşmak için önerilen kullanıcı yok"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Uygulama listesi"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu içerik, iş uygulamalarıyla açılamaz"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu içerik, kişisel uygulamalarla paylaşılamaz"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu içerik, kişisel uygulamalarla açılamaz"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"İş profili duraklatıldı"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Açmak için dokunun"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"İş uygulamaları duraklatıldı"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Devam ettir"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"İş uygulaması yok"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Kişisel uygulama yok"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> uygulaması kişisel profilinizde açılsın mı?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Metni dahil et"</string>      <string name="exclude_link" msgid="1332778255031992228">"Bağlantıyı hariç tut"</string>      <string name="include_link" msgid="827855767220339802">"Bağlantıyı dahil et"</string> +    <string name="pinned" msgid="7623664001331394139">"Sabitlendi"</string>  </resources> diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml index 8a744661..f9d810af 100644 --- a/java/res/values-uk/strings.xml +++ b/java/res/values-uk/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Закріпити додаток <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Відкріпити додаток <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Редагувати"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} і ще # файл}one{{file_name} і ще # файл}few{{file_name} і ще # файли}many{{file_name} і ще # файлів}other{{file_name} і ще # файлу}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{і ще # файл}one{і ще # файл}few{і ще # файли}many{і ще # файлів}other{і ще # файлу}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{і ще # файл}one{і ще # файл}few{і ще # файли}many{і ще # файлів}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{Надсилаються # зображення}many{Надсилаються # зображень}other{Надсилається # зображення}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Надсилається відео}one{Надсилається # відео}few{Надсилаються # відео}many{Надсилаються # відео}other{Надсилається # відео}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Надсилається # об’єкт}one{Надсилається # об’єкт}few{Надсилаються # об’єкти}many{Надсилаються # об’єктів}other{Надсилається # об’єкта}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Надсил. зображ. з текстом"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Надсил. зображ. з посил."</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Надсилається # файл}one{Надсилається # файл}few{Надсилаються # файли}many{Надсилаються # файлів}other{Надсилається # файлу}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Надсилання зображення з текстом}one{Надсилання # зображення з текстом}few{Надсилання # зображень із текстом}many{Надсилання # зображень із текстом}other{Надсилання # зображення з текстом}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Надсилання зображення з посиланням}one{Надсилання # зображення з посиланням}few{Надсилання # зображень із посиланням}many{Надсилання # зображень із посиланням}other{Надсилання # зображення з посиланням}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Надсилання відео з текстом}one{Надсилання # відео з текстом}few{Надсилання # відео з текстом}many{Надсилання # відео з текстом}other{Надсилання # відео з текстом}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Зображення для попереднього перегляду фото чи малюнка"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Зображення для попереднього перегляду відео"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Зображення для попереднього перегляду файлу"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Немає рекомендацій про те, з ким поділитися"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список додатків"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Цей контент не можна відкривати в робочих додатках"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Цим контентом не можна ділитися в особистих додатках"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Цей контент не можна відкривати в особистих додатках"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Робочий профіль призупинено"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Торкніться, щоб увімкнути"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Робочі додатки призупинено"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Увімкнути знову"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Немає робочих додатків"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Немає особистих додатків"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Відкрити додаток <xliff:g id="APP">%s</xliff:g> в особистому профілі?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Додати текст"</string>      <string name="exclude_link" msgid="1332778255031992228">"Вилучити посилання"</string>      <string name="include_link" msgid="827855767220339802">"Додати посилання"</string> +    <string name="pinned" msgid="7623664001331394139">"Закріплено"</string>  </resources> diff --git a/java/res/values-ur/strings.xml b/java/res/values-ur/strings.xml index 493ffef4..6a101d98 100644 --- a/java/res/values-ur/strings.xml +++ b/java/res/values-ur/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> کو پن کریں"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> سے پن ہٹائیں"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"ترمیم کریں"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # فائل}other{{file_name} + # فائلز}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # فائل}other{+ # فائلز}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ #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_videos" msgid="3583423190182877434">"{count,plural, =1{ویڈیو کا اشتراک کیا جا رہا ہے}other{# ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# آئٹم کا اشتراک کیا جا رہا ہے}other{# آئٹمز کا اشتراک کیا جا رہا ہے}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"ٹیکسٹ کے ساتھ تصویر کا اشتراک کیا جا رہا ہے"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"لنک کے ساتھ تصویر کا اشتراک کیا جا رہا ہے"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{لنک کے ساتھ تصویر کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # تصاویر کا اشتراک کیا جا رہا ہے}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ٹیکسٹ کے ساتھ ویڈیو کا اشتراک کیا جا رہا ہے}other{ٹیکسٹ کے ساتھ # ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"تصویر کے پیش منظر کا تھمب نیل"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"ویڈیو کے پیش منظر کا تھمب نیل"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"فائل کے پیش منظر کا تھمب نیل"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"اشتراک کرنے کے لیے کوئی تجویز کردہ لوگ نہیں"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ایپس کی فہرست"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"اس مواد کو ورک ایپس کے ساتھ نہیں کھولا جا سکتا"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"اس مواد کا اشتراک ذاتی ایپس کے ساتھ نہیں کیا جا سکتا"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"اس مواد کو ذاتی ایپس کے ساتھ نہیں کھولا جا سکتا"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"دفتری پروفائل روک دی گئی ہے"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"آن کرنے کیلئے تھپتھپائیں"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ورک ایپس موقوف ہیں"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"غیر موقوف کریں"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"کوئی ورک ایپ نہیں"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"کوئی ذاتی ایپ نہیں"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"اپنی ذاتی پروفائل میں <xliff:g id="APP">%s</xliff:g> کھولیں؟"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"ٹیکسٹ شامل کریں"</string>      <string name="exclude_link" msgid="1332778255031992228">"لنک خارج کریں"</string>      <string name="include_link" msgid="827855767220339802">"لنک شامل کریں"</string> +    <string name="pinned" msgid="7623664001331394139">"پن کردہ"</string>  </resources> diff --git a/java/res/values-uz/strings.xml b/java/res/values-uz/strings.xml index 2596c7cc..24249f50 100644 --- a/java/res/values-uz/strings.xml +++ b/java/res/values-uz/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Mahkamlash: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Yechib olish: <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Tahrirlash"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ta fayl}other{{file_name} + # ta fayl}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ta fayl}other{+ # ta fayl}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ yana # ta fayl}other{+ yana # ta fayl}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Matn ulashilmoqda"</string>      <string name="sharing_link" msgid="2307694372813942916">"Havola ulashilmoqda"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Rasm ulashilmoqda}other{# ta rasm ulashilmoqda}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video ulashilmoqda}other{# ta video ulashilmoqda}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ta fayl ulashilmoqda}other{# ta fayl ulashilmoqda}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Matnli havola ulashilmoqda"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Havolali rasm ulashilmoqda"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ta fayl ulashilmoqda}other{# ta fayl ulashilmoqda}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Matnli havolani yuborish}other{# ta matnli havolani yuborish}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Havolali rasmni yuborish}other{# ta havolali rasmni yuborish}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Matnli videoni yuborish}other{# ta matnli videoni yuborish}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Rasmga razm solish eskizi"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Videoga razm solish eskizi"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faylga razm solish eskizi"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ulashish uchun hech kim tavsiya qilinmagan"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Ilovalar roʻyxati"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontent ishga oid ilovalar bilan ochilmaydi"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu kontent shaxsiy ilovalar bilan ulashilmaydi"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu kontent shaxsiy ilovalar bilan ochilmaydi"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Ish profili pauzada"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Yoqish uchun bosing"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ishga oid ilovalar pauza qilingan"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Davom ettirish"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ishga oid ilovalar topilmadi"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Shaxsiy ilovalar topilmadi"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> shaxsiy profilda ochilsinmi?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Matnni kiritish"</string>      <string name="exclude_link" msgid="1332778255031992228">"Havolani chiqarib tashlash"</string>      <string name="include_link" msgid="827855767220339802">"Havolani kiritish"</string> +    <string name="pinned" msgid="7623664001331394139">"Mahkamlangan"</string>  </resources> diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml index ed649986..b08d9a3a 100644 --- a/java/res/values-vi/strings.xml +++ b/java/res/values-vi/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Ghim <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Bỏ ghim <xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Chỉnh sửa"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # tệp}other{{file_name} + # tệp}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # tệp}other{+ # tệp}}"</string> -    <string name="sharing_text" msgid="8137537443603304062">"Đang chia sẻ văn bản"</string> -    <string name="sharing_link" msgid="2307694372813942916">"Đang chia sẻ liên kết"</string> -    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Đang chia sẻ hình ảnh}other{Đang chia sẻ # hình ảnh}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # tệp khác}other{+ # tệp khác}}"</string> +    <string name="sharing_text" msgid="8137537443603304062">"Chia sẻ văn bản"</string> +    <string name="sharing_link" msgid="2307694372813942916">"Chia sẻ đường liên kết"</string> +    <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Chia sẻ hình ảnh}other{Chia sẻ # hình ảnh}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Đang chia sẻ video}other{Đang chia sẻ # video}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Đang chia sẻ # mục}other{Đang chia sẻ # mục}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Đang chia sẻ hình ảnh có văn bản"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Đang chia sẻ hình ảnh có liên kết"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Đang chia sẻ # tệp}other{Đang chia sẻ # tệp}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Đang chia sẻ hình ảnh có văn bản}other{Đang chia sẻ # hình ảnh có văn bản}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Đang chia sẻ hình ảnh có đường liên kết}other{Đang chia sẻ # hình ảnh có đường liên kết}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Đang chia sẻ video có văn bản}other{Đang chia sẻ # video có văn bản}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Hình thu nhỏ của ảnh xem trước"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Hình thu nhỏ của video xem trước"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Hình thu nhỏ xem trước tệp"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Không có gợi ý nào về người mà bạn có thể chia sẻ"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Danh sách ứng dụng"</string>      <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> @@ -74,16 +83,17 @@      <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>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bạn không thể chia sẻ nội dung này bằng ứng dụng cá nhân"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bạn không thể mở nội dung này bằng ứng dụng cá nhân"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Hồ sơ công việc đã bị tạm dừng"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Nhấn để bật"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Các ứng dụng công việc đã bị tạm dừng"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Tiếp tục"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Không có ứng dụng công việc"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Không có ứng dụng cá nhân"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Mở <xliff:g id="APP">%s</xliff:g> trong hồ sơ cá nhân của bạn?"</string>      <string name="miniresolver_open_in_work" msgid="4271638122142624693">"Mở <xliff:g id="APP">%s</xliff:g> trong hồ sơ công việc của bạn?"</string>      <string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Dùng trình duyệt cá nhân"</string>      <string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Dùng trình duyệt công việc"</string> -    <string name="exclude_text" msgid="5508128757025928034">"Loại trừ văn bản"</string> +    <string name="exclude_text" msgid="5508128757025928034">"Không kèm văn bản"</string>      <string name="include_text" msgid="642280283268536140">"Thêm văn bản"</string> -    <string name="exclude_link" msgid="1332778255031992228">"Loại trừ đường liên kết"</string> +    <string name="exclude_link" msgid="1332778255031992228">"Không kèm đường liên kết"</string>      <string name="include_link" msgid="827855767220339802">"Thêm đường liên kết"</string> +    <string name="pinned" msgid="7623664001331394139">"Đã ghim"</string>  </resources> diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml index 4541bea6..e208e106 100644 --- a/java/res/values-zh-rCN/strings.xml +++ b/java/res/values-zh-rCN/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"固定<xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"取消置顶<xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"编辑"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} 以及另外 # 个文件}other{{file_name} 以及另外 # 个文件}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享视频}other{正在分享 # 个视频}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 个项目}other{正在分享 # 个项目}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享带有文本的图片"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享带有链接的图片"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{正在分享带有链接的图片}other{正在分享带有链接的 # 个图片}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{正在分享带有文本的视频}other{正在分享带有文本的 # 个视频}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"图片预览缩略图"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"视频预览缩略图"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"文件预览缩略图"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"没有任何推荐的分享对象"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"应用列表"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"无法使用工作应用打开该内容"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"无法使用个人应用分享该内容"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"无法使用个人应用打开该内容"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作资料已被暂停"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"点按即可开启"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"工作应用已暂停"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"解除暂停"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"没有支持该内容的工作应用"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"没有支持该内容的个人应用"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要使用个人资料打开 <xliff:g id="APP">%s</xliff:g> 吗?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"包括文本"</string>      <string name="exclude_link" msgid="1332778255031992228">"排除链接"</string>      <string name="include_link" msgid="827855767220339802">"包括链接"</string> +    <string name="pinned" msgid="7623664001331394139">"已固定"</string>  </resources> diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml index 1a5fcc33..837b1587 100644 --- a/java/res/values-zh-rHK/strings.xml +++ b/java/res/values-zh-rHK/strings.xml @@ -45,37 +45,46 @@      <string name="use_a_different_app" msgid="2062380818535918975">"使用不同的應用程式"</string>      <string name="chooseActivity" msgid="6659724877523973446">"選擇一項操作"</string>      <string name="noApplications" msgid="1139487441772284671">"沒有應用程式可執行這項操作。"</string> -    <string name="forward_intent_to_owner" msgid="6454987608971162379">"您目前並未透過公司檔案使用這個應用程式"</string> -    <string name="forward_intent_to_work" msgid="2906094223089139419">"您目前透過公司檔案使用這個應用程式"</string> +    <string name="forward_intent_to_owner" msgid="6454987608971162379">"你目前並未透過公司檔案使用這個應用程式"</string> +    <string name="forward_intent_to_work" msgid="2906094223089139419">"你目前透過公司檔案使用這個應用程式"</string>      <string name="activity_resolver_use_always" msgid="8674194687637555245">"一律採用"</string>      <string name="activity_resolver_use_once" msgid="594173435998892989">"只此一次"</string>      <string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"<xliff:g id="APP">%1$s</xliff:g> 不支援工作設定檔"</string>      <string name="pin_specific_target" msgid="5057063421361441406">"固定<xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"取消將<xliff:g id="LABEL">%1$s</xliff:g>置頂"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"編輯"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{「{file_name}」和另外 # 個檔案}other{「{file_name}」和另外 # 個檔案}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享影片}other{正在分享 # 部影片}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 個項目}other{正在分享 # 個項目}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享圖片 (含有文字)"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享圖片 (含有連結)"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{正在分享圖片 (含有連結)}other{正在分享 # 張圖片 (含有連結)}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{正在分享影片 (含有文字)}other{正在分享 # 部影片 (含有文字)}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"圖片預覽縮圖"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"影片預覽縮圖"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"檔案預覽縮圖"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"沒有推薦的分享對象"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"應用程式清單"</string>      <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_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>      <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string> -    <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"已被您的 IT 管理員封鎖"</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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法使用個人應用程式分享此內容"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法與個人應用程式分享此內容"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"無法使用個人應用程式開啟此內容"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作設定檔已暫停使用"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"輕按即可啟用"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"已暫停工作應用程式"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"取消暫停"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"沒有適用的工作應用程式"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"沒有適用的個人應用程式"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人設定檔中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"加入文字"</string>      <string name="exclude_link" msgid="1332778255031992228">"不包括連結"</string>      <string name="include_link" msgid="827855767220339802">"加入連結"</string> +    <string name="pinned" msgid="7623664001331394139">"固定咗"</string>  </resources> diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml index 29003473..0fddc70e 100644 --- a/java/res/values-zh-rTW/strings.xml +++ b/java/res/values-zh-rTW/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"將「<xliff:g id="LABEL">%1$s</xliff:g>」置頂"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"將「<xliff:g id="LABEL">%1$s</xliff:g>」取消固定"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"編輯"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{「{file_name}」和另外 # 個檔案}other{「{file_name}」和另外 # 個檔案}}"</string>      <string name="other_files" msgid="4501185823517473875">"{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="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_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享影片}other{正在分享 # 部影片}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 個項目}other{正在分享 # 個項目}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享含有文字的圖片"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享含有連結的圖片"</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> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{分享含有連結的圖片}other{分享 # 張含有連結的圖片}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{分享含有文字的影片}other{分享 # 部含有文字的影片}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"圖片預覽縮圖"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"影片預覽縮圖"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"檔案預覽縮圖"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"沒有建議的分享對象"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"應用程式清單"</string>      <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> @@ -72,10 +81,10 @@      <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> -    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法透過個人應用程式分享這項內容"</string> +    <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法與個人應用程式分享這項內容"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"無法使用個人應用程式開啟這項內容"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作資料夾已暫停使用"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"輕觸即可啟用"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"工作應用程式目前為暫停狀態"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"取消暫停"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"沒有適用的工作應用程式"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"沒有適用的個人應用程式"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人資料夾中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"加回文字"</string>      <string name="exclude_link" msgid="1332778255031992228">"排除連結"</string>      <string name="include_link" msgid="827855767220339802">"加回連結"</string> +    <string name="pinned" msgid="7623664001331394139">"已固定"</string>  </resources> diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml index 9e1a207f..b651eb06 100644 --- a/java/res/values-zu/strings.xml +++ b/java/res/values-zu/strings.xml @@ -53,17 +53,26 @@      <string name="pin_specific_target" msgid="5057063421361441406">"Phina i-<xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="unpin_specific_target" msgid="3115158908159857777">"Susa ukuphina ku-<xliff:g id="LABEL">%1$s</xliff:g>"</string>      <string name="screenshot_edit" msgid="3857183660047569146">"Hlela"</string> -    <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ifayela elingu-#}one{{file_name} + amafayela angu-#}other{{file_name} + amafayela angu-#}}"</string>      <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Ifayela eli-+ #}one{Amafayela angu-+ #}other{Amafayela angu-+ #}}"</string> +    <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Ifayela elengeziwe eli-+ #}one{Amafayela engeziwe angu-+ #}other{Amafayela engeziwe angu-+ #}}"</string>      <string name="sharing_text" msgid="8137537443603304062">"Yabelana ngombhalo"</string>      <string name="sharing_link" msgid="2307694372813942916">"Yabelana ngelinki"</string>      <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Yabelana ngomfanekiso}one{Yabelana ngemifanekiso engu-#}other{Yabelana ngemifanekiso engu-#}}"</string>      <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Yabelana ngevidiyo}one{Yabelana ngamavidiyo angu-#}other{Yabelana ngamavidiyo angu-#}}"</string> -    <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Yabelana ngento engu-#}one{Yabelana ngezinto ezingu-#}other{Yabelana ngezinto ezingu-#}}"</string> -    <string name="sharing_image_with_text" msgid="3844438616236662145">"Yabelana ngomfanekiso ngombhalo"</string> -    <string name="sharing_image_with_link" msgid="5318319026387721227">"Yabelana ngomfanekiso ngelinki"</string> +    <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Yabelana ngefayela eli-#}one{Yabelana ngamafayela angu-#}other{Yabelana ngamafayela angu-#}}"</string> +    <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Yabelana ngomfanekiso ngombhalo}one{Yabelana ngemifanekiso engu-# ngombhalo}other{Yabelana ngemifanekiso engu-# ngombhalo}}"</string> +    <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Yabelana ngomfanekiso ngelinki}one{Yabelana ngemifanekiso engu-# ngelinki}other{Yabelana ngemifanekiso engu-# ngelinki}}"</string> +    <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Yabelana ngevidiyo ngombhalo}one{Yabelana ngamavidiyo angu-# ngombhalo}other{Yabelana ngamavidiyo angu-# ngombhalo}}"</string> +    <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_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> +    <string name="image_preview_a11y_description" msgid="297102643932491797">"Isithonjana sokuhlola kuqala umfanekiso"</string> +    <string name="video_preview_a11y_description" msgid="683440858811095990">"Isithonjana sokuhlola kuqala ividiyo"</string> +    <string name="file_preview_a11y_description" msgid="7397224827802410602">"Isithonjana sokuhlola kuqala ifayela"</string>      <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ayinconyelwa ukuba abantu bayabelane"</string> -    <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Uhlu lwezinhlelo zokusebenza"</string>      <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> @@ -74,8 +83,8 @@      <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womsebenzi"</string>      <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Lokhu okuqukethwe akukwazi ukwabiwa nama-app womuntu siqu"</string>      <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womuntu siqu"</string> -    <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Iphrofayela yomsebenzi iphunyuziwe"</string> -    <string name="resolver_switch_on_work" msgid="4615505942222617333">"Thepha ukuze uvule"</string> +    <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ama-app omsebenzi amisiwe"</string> +    <string name="resolver_switch_on_work" msgid="8678893259344318807">"Qhubekisa"</string>      <string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Awekho ama-app womsebenzi"</string>      <string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Awekho ama-app womuntu siqu"</string>      <string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vula i-<xliff:g id="APP">%s</xliff:g> kwiphrofayela yakho siqu?"</string> @@ -86,4 +95,5 @@      <string name="include_text" msgid="642280283268536140">"Faka umbhalo"</string>      <string name="exclude_link" msgid="1332778255031992228">"Ungafaki ilinki"</string>      <string name="include_link" msgid="827855767220339802">"Faka ilinki"</string> +    <string name="pinned" msgid="7623664001331394139">"Kuphiniwe"</string>  </resources> diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml index 67acb3ae..c9f2c300 100644 --- a/java/res/values/attrs.xml +++ b/java/res/values/attrs.xml @@ -32,6 +32,11 @@               will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the               top limit the ignoreOffset elements. -->          <attr name="ignoreOffsetTopLimit" format="reference" /> +        <!-- Specifies whether ResolverDrawerLayout should use an alternative nested fling logic +        adjusted for the scrollable preview feature. +        Controlled by the flag com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW. +        --> +        <attr name="useScrollablePreviewNestedFlingLogic" format="boolean" />      </declare-styleable>      <declare-styleable name="ResolverDrawerLayout_LayoutParams"> diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index ae80815b..8843c81a 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -33,7 +33,6 @@      <dimen name="chooser_preview_image_max_dimen">200dp</dimen>      <dimen name="chooser_header_scroll_elevation">4dp</dimen>      <dimen name="chooser_max_collapsed_height">288dp</dimen> -    <dimen name="chooser_direct_share_label_placeholder_max_width">72dp</dimen>      <dimen name="chooser_icon_size">56dp</dimen>      <dimen name="chooser_badge_size">22dp</dimen>      <dimen name="resolver_icon_size">32dp</dimen> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 4b5367c0..0c772573 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -303,4 +303,8 @@      <string name="exclude_link">Exclude link</string>      <!-- Title for a button. Adds back a (previously excluded) web link into the shared content. -->      <string name="include_link">Include link</string> + +    <!-- Accesssibility content description for a sharesheet target that has been pinned to the +         front of the list by the user. [CHAR LIMIT=NONE] --> +    <string name="pinned">Pinned</string>  </resources> diff --git a/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt b/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt deleted file mode 100644 index 5067c0ee..00000000 --- a/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.flags - -import android.util.SparseBooleanArray -import androidx.annotation.GuardedBy -import com.android.systemui.flags.BooleanFlag -import com.android.systemui.flags.FlagManager -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag -import javax.annotation.concurrent.ThreadSafe - -@ThreadSafe -internal class DebugFeatureFlagRepository( -    private val flagManager: FlagManager, -    private val deviceConfig: DeviceConfigProxy, -) : FeatureFlagRepository { -    @GuardedBy("self") -    private val cache = hashMapOf<String, Boolean>() - -    override fun isEnabled(flag: UnreleasedFlag): Boolean = isFlagEnabled(flag) - -    override fun isEnabled(flag: ReleasedFlag): Boolean = isFlagEnabled(flag) - -    private fun isFlagEnabled(flag: BooleanFlag): Boolean { -        synchronized(cache) { -            cache[flag.name]?.let { return it } -        } -        val flagValue = readFlagValue(flag) -        return synchronized(cache) { -            // the first read saved in the cache wins -            cache.getOrPut(flag.name) { flagValue } -        } -    } - -    private fun readFlagValue(flag: BooleanFlag): Boolean { -        val localOverride = runCatching { -            flagManager.isEnabled(flag.name) -        }.getOrDefault(null) -        val remoteOverride = deviceConfig.isEnabled(flag) - -        // Only check for teamfood if the default is false -        // and there is no server override. -        if (remoteOverride == null -            && !flag.default -            && localOverride == null -            && !flag.isTeamfoodFlag -            && flag.teamfood -        ) { -            return flagManager.isTeamfoodEnabled -        } -        return localOverride ?: remoteOverride ?: flag.default -    } - -    companion object { -        /** keep in sync with [com.android.systemui.flags.Flags] */ -        private const val TEAMFOOD_FLAG_NAME = "teamfood" - -        private val BooleanFlag.isTeamfoodFlag: Boolean -            get() = name == TEAMFOOD_FLAG_NAME - -        private val FlagManager.isTeamfoodEnabled: Boolean -            get() = runCatching { -                isEnabled(TEAMFOOD_FLAG_NAME) ?: false -            }.getOrDefault(false) -    } -} diff --git a/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt deleted file mode 100644 index 4ddb0447..00000000 --- a/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.flags - -import android.content.Context -import android.os.Handler -import android.os.Looper -import com.android.systemui.flags.FlagManager - -class FeatureFlagRepositoryFactory { -    fun create(context: Context): FeatureFlagRepository = -        DebugFeatureFlagRepository( -            FlagManager(context, Handler(Looper.getMainLooper())), -            DeviceConfigProxy(), -        ) -} diff --git a/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt b/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt deleted file mode 100644 index f9fa2c6a..00000000 --- a/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.flags - -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag -import javax.annotation.concurrent.ThreadSafe - -@ThreadSafe -internal class ReleaseFeatureFlagRepository( -    private val deviceConfig: DeviceConfigProxy, -) : FeatureFlagRepository { -    override fun isEnabled(flag: UnreleasedFlag): Boolean = flag.default - -    override fun isEnabled(flag: ReleasedFlag): Boolean = -        deviceConfig.isEnabled(flag) ?: flag.default -} diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java index 168f36d6..3565e757 100644 --- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java +++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java @@ -16,12 +16,12 @@  package com.android.intentresolver; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.ActivityManager;  import android.os.UserHandle;  import android.os.UserManager; +import androidx.annotation.Nullable;  import androidx.annotation.VisibleForTesting;  /** @@ -35,7 +35,7 @@ public final class AnnotatedUserHandles {      /**       * The {@link UserHandle} that launched Sharesheet.       * TODO: I believe this would always be the handle corresponding to {@code userIdOfCallingApp} -     * except possibly if the caller used {@link Activity#startActivityAsUser()} to launch +     * except possibly if the caller used {@link Activity#startActivityAsUser} to launch       * Sharesheet as a different user than they themselves were running as. Verify and document.       */      public final UserHandle userHandleSharesheetLaunchedAs; @@ -57,21 +57,21 @@ public final class AnnotatedUserHandles {      /**       * The {@link UserHandle} that owns the "work tab" in a tabbed share UI. This is (an arbitrary) -     * one of the "managed" profiles associated with {@link personalProfileUserHandle}. +     * one of the "managed" profiles associated with {@link #personalProfileUserHandle}.       */      @Nullable      public final UserHandle workProfileUserHandle;      /** -     * The {@link UserHandle} of the clone profile belonging to {@link personalProfileUserHandle}. +     * The {@link UserHandle} of the clone profile belonging to {@link #personalProfileUserHandle}.       */      @Nullable      public final UserHandle cloneProfileUserHandle;      /** -     * The "tab owner" user handle (i.e., either {@link personalProfileUserHandle} or -     * {@link workProfileUserHandle}) that either matches or owns the profile of the -     * {@link userHandleSharesheetLaunchedAs}. +     * The "tab owner" user handle (i.e., either {@link #personalProfileUserHandle} or +     * {@link #workProfileUserHandle}) that either matches or owns the profile of the +     * {@link #userHandleSharesheetLaunchedAs}.       *       * In the current implementation, we can assert that this is the same as       * `userHandleSharesheetLaunchedAs` except when the latter is the clone profile; then this is @@ -105,7 +105,7 @@ public final class AnnotatedUserHandles {                  .build();      } -    @VisibleForTesting static Builder newBuilder() { +    @VisibleForTesting public static Builder newBuilder() {          return new Builder();      } @@ -173,7 +173,7 @@ public final class AnnotatedUserHandles {      }      @VisibleForTesting -    static class Builder { +    public static class Builder {          private int mUserIdOfCallingApp;          private UserHandle mUserHandleSharesheetLaunchedAs;          private UserHandle mPersonalProfileUserHandle; diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index a54e8c62..310fcc27 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -16,7 +16,6 @@  package com.android.intentresolver; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.ActivityOptions;  import android.app.PendingIntent; @@ -34,6 +33,8 @@ import android.text.TextUtils;  import android.util.Log;  import android.view.View; +import androidx.annotation.Nullable; +  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; @@ -98,12 +99,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      private final @Nullable ChooserAction mModifyShareAction;      private final Consumer<Boolean> mExcludeSharedTextAction;      private final Consumer</* @Nullable */ Integer> mFinishCallback; -    private final EventLog mLogger; +    private final EventLog mLog;      /**       * @param context       * @param chooserRequest data about the invocation of the current Sharesheet session. -     * @param integratedDeviceComponents info about other components that are available on this       * device to implement the supported action types.       * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"       * setting is updated. The argument is whether the shared text is to be excluded. @@ -117,7 +117,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              Context context,              ChooserRequestParameters chooserRequest,              ChooserIntegratedDeviceComponents integratedDeviceComponents, -            EventLog logger, +            EventLog log,              Consumer<Boolean> onUpdateSharedTextIsExcluded,              Callable</* @Nullable */ View> firstVisibleImageQuery,              ActionActivityStarter activityStarter, @@ -129,7 +129,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                          chooserRequest.getTargetIntent(),                          chooserRequest.getReferrerPackageName(),                          finishCallback, -                        logger), +                        log),                  makeEditButtonRunnable(                          getEditSharingTarget(                                  context, @@ -137,11 +137,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                                  integratedDeviceComponents),                          firstVisibleImageQuery,                          activityStarter, -                        logger), +                        log),                  chooserRequest.getChooserActions(),                  chooserRequest.getModifyShareAction(),                  onUpdateSharedTextIsExcluded, -                logger, +                log,                  finishCallback);      } @@ -153,7 +153,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              List<ChooserAction> customActions,              @Nullable ChooserAction modifyShareAction,              Consumer<Boolean> onUpdateSharedTextIsExcluded, -            EventLog logger, +            EventLog log,              Consumer</* @Nullable */ Integer> finishCallback) {          mContext = context;          mCopyButtonRunnable = copyButtonRunnable; @@ -161,7 +161,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio          mCustomActions = ImmutableList.copyOf(customActions);          mModifyShareAction = modifyShareAction;          mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; -        mLogger = logger; +        mLog = log;          mFinishCallback = finishCallback;      } @@ -188,7 +188,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                      mCustomActions.get(i),                      mFinishCallback,                      () -> { -                        mLogger.logCustomActionSelected(position); +                        mLog.logCustomActionSelected(position);                      }              );              if (actionRow != null) { @@ -209,7 +209,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                  mModifyShareAction,                  mFinishCallback,                  () -> { -                    mLogger.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE); +                    mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);                  });      } @@ -233,13 +233,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              Intent targetIntent,              String referrerPackageName,              Consumer<Integer> finishCallback, -            EventLog logger) { +            EventLog log) {          final ClipData clipData;          try {              clipData = extractTextToCopy(targetIntent);          } catch (Throwable t) {              Log.e(TAG, "Failed to extract data to copy", t); -            return  null; +            return null;          }          if (clipData == null) {              return null; @@ -249,7 +249,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                      Context.CLIPBOARD_SERVICE);              clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName); -            logger.logActionSelected(EventLog.SELECTION_TYPE_COPY); +            log.logActionSelected(EventLog.SELECTION_TYPE_COPY);              finishCallback.accept(Activity.RESULT_OK);          };      } @@ -317,8 +317,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                  ri,                  context.getString(R.string.screenshot_edit),                  "", -                resolveIntent, -                null); +                resolveIntent);          dri.getDisplayIconHolder().setDisplayIcon(                  context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));          return dri; @@ -328,10 +327,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              TargetInfo editSharingTarget,              Callable</* @Nullable */ View> firstVisibleImageQuery,              ActionActivityStarter activityStarter, -            EventLog logger) { +            EventLog log) {          return () -> {              // Log share completion via edit. -            logger.logActionSelected(EventLog.SELECTION_TYPE_EDIT); +            log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);              View firstImageView = null;              try { @@ -373,10 +372,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                                  null,                                  null,                                  ActivityOptions.makeCustomAnimation( -                                        context, -                                        R.anim.slide_in_right, -                                        R.anim.slide_out_left) -                                                .toBundle()); +                                                context, +                                                R.anim.slide_in_right, +                                                R.anim.slide_out_left) +                                        .toBundle());                      } catch (PendingIntent.CanceledException e) {                          Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");                      } diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index b27f054e..9000ab3a 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -24,10 +24,10 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROS  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 androidx.lifecycle.LifecycleKt.getCoroutineScope; +  import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; -import android.annotation.IntDef; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.ActivityManager;  import android.app.ActivityOptions; @@ -51,11 +51,9 @@ import android.database.Cursor;  import android.graphics.Insets;  import android.net.Uri;  import android.os.Bundle; -import android.os.Environment;  import android.os.SystemClock;  import android.os.UserHandle;  import android.os.UserManager; -import android.os.storage.StorageManager;  import android.service.chooser.ChooserTarget;  import android.util.Log;  import android.util.Slog; @@ -67,15 +65,15 @@ import android.view.ViewTreeObserver;  import android.view.WindowInsets;  import android.widget.TextView; +import androidx.annotation.IntDef;  import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import androidx.lifecycle.ViewModelProvider;  import androidx.recyclerview.widget.GridLayoutManager;  import androidx.recyclerview.widget.RecyclerView;  import androidx.viewpager.widget.ViewPager; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.MultiDisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo; @@ -83,8 +81,10 @@ import com.android.intentresolver.contentpreview.BasePreviewViewModel;  import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;  import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;  import com.android.intentresolver.contentpreview.PreviewViewModel; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; +import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;  import com.android.intentresolver.grid.ChooserGridAdapter;  import com.android.intentresolver.icons.DefaultTargetDataLoader;  import com.android.intentresolver.icons.TargetDataLoader; @@ -100,7 +100,8 @@ import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.content.PackageMonitor;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import java.io.File; +import dagger.hilt.android.AndroidEntryPoint; +  import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.text.Collator; @@ -115,12 +116,15 @@ import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors;  import java.util.function.Consumer; +import javax.inject.Inject; +  /**   * The Chooser Activity handles intent resolution specifically for sharing intents -   * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.   *   */ -public class ChooserActivity extends ResolverActivity implements +@AndroidEntryPoint(ResolverActivity.class) +public class ChooserActivity extends Hilt_ChooserActivity implements          ResolverListAdapter.ResolverListCommunicator {      private static final String TAG = "ChooserActivity"; @@ -161,7 +165,7 @@ public class ChooserActivity extends ResolverActivity implements      private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;      private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; -    @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = { +    @IntDef({              TARGET_TYPE_DEFAULT,              TARGET_TYPE_CHOOSER_TARGET,              TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, @@ -170,6 +174,9 @@ public class ChooserActivity extends ResolverActivity implements      @Retention(RetentionPolicy.SOURCE)      public @interface ShareTargetType {} +    @Inject public FeatureFlags mFeatureFlags; +    @Inject public EventLog mEventLog; +      private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;      /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the @@ -183,13 +190,9 @@ public class ChooserActivity extends ResolverActivity implements      private ChooserRefinementManager mRefinementManager; -    private FeatureFlagRepository mFeatureFlagRepository;      private ChooserContentPreviewUi mChooserContentPreviewUi;      private boolean mShouldDisplayLandscape; -    // statsd logger wrapper -    protected EventLog mEventLog; -      private long mChooserShownTime;      protected boolean mIsSuccessfullySelected; @@ -229,31 +232,52 @@ public class ChooserActivity extends ResolverActivity implements       */      private boolean mFinishWhenStopped = false; -    public ChooserActivity() {} -      @Override      protected void onCreate(Bundle savedInstanceState) {          Tracer.INSTANCE.markLaunched();          final long intentReceivedTime = System.currentTimeMillis();          mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); -        getEventLog().logSharesheetTriggered(); - -        mFeatureFlagRepository = createFeatureFlagRepository(); -        mIntegratedDeviceComponents = getIntegratedDeviceComponents(); -          try {              mChooserRequest = new ChooserRequestParameters(                      getIntent(),                      getReferrerPackageName(), -                    getReferrer(), -                    mFeatureFlagRepository); +                    getReferrer());          } catch (IllegalArgumentException e) {              Log.e(TAG, "Caller provided invalid Chooser request parameters", e);              finish();              super_onCreate(null);              return;          } +        mPinnedSharedPrefs = getPinnedSharedPrefs(this); +        mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); +        mShouldDisplayLandscape = +                shouldDisplayLandscape(getResources().getConfiguration().orientation); +        setRetainInOnStop(mChooserRequest.shouldRetainInOnStop()); + +        createProfileRecords( +                new AppPredictorFactory( +                        this, +                        mChooserRequest.getSharedText(), +                        mChooserRequest.getTargetIntentFilter()), +                mChooserRequest.getTargetIntentFilter()); + + +        super.onCreate( +                savedInstanceState, +                mChooserRequest.getTargetIntent(), +                mChooserRequest.getAdditionalTargets(), +                mChooserRequest.getTitle(), +                mChooserRequest.getDefaultTitleResource(), +                mChooserRequest.getInitialIntents(), +                /* resolutionList= */ null, +                /* supportsAlwaysUseOption= */ false, +                new DefaultTargetDataLoader(this, getLifecycle(), false), +                /* safeForwardingMode= */ true); + +        getEventLog().logSharesheetTriggered(); + +        mIntegratedDeviceComponents = getIntegratedDeviceComponents();          mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); @@ -279,39 +303,21 @@ public class ChooserActivity extends ResolverActivity implements                  new ViewModelProvider(this, createPreviewViewModelFactory())                          .get(BasePreviewViewModel.class);          mChooserContentPreviewUi = new ChooserContentPreviewUi( -                getLifecycle(), -                previewViewModel.createOrReuseProvider(mChooserRequest), +                getCoroutineScope(getLifecycle()), +                previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()),                  mChooserRequest.getTargetIntent(),                  previewViewModel.createOrReuseImageLoader(),                  createChooserActionFactory(),                  mEnterTransitionAnimationDelegate,                  new HeadlineGeneratorImpl(this)); -        mPinnedSharedPrefs = getPinnedSharedPrefs(this); - -        mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); -        mShouldDisplayLandscape = -                shouldDisplayLandscape(getResources().getConfiguration().orientation); -        setRetainInOnStop(mChooserRequest.shouldRetainInOnStop()); - -        createProfileRecords( -                new AppPredictorFactory( -                        getApplicationContext(), -                        mChooserRequest.getSharedText(), -                        mChooserRequest.getTargetIntentFilter()), -                mChooserRequest.getTargetIntentFilter()); - -        super.onCreate( -                savedInstanceState, -                mChooserRequest.getTargetIntent(), -                mChooserRequest.getAdditionalTargets(), -                mChooserRequest.getTitle(), -                mChooserRequest.getDefaultTitleResource(), -                mChooserRequest.getInitialIntents(), -                /* resolutionList= */ null, -                /* supportsAlwaysUseOption= */ false, -                new DefaultTargetDataLoader(this, getLifecycle(), false), -                /* safeForwardingMode= */ true); +        updateStickyContentPreview(); +        if (shouldShowStickyContentPreview() +                || mChooserMultiProfilePagerAdapter +                .getCurrentRootAdapter().getSystemRowCount() != 0) { +            getEventLog().logActionShareWithPreview( +                    mChooserContentPreviewUi.getPreferredContentPreview()); +        }          mChooserShownTime = System.currentTimeMillis();          final long systemCost = mChooserShownTime - intentReceivedTime; @@ -358,19 +364,15 @@ public class ChooserActivity extends ResolverActivity implements          return R.style.Theme_DeviceDefault_Chooser;      } -    protected FeatureFlagRepository createFeatureFlagRepository() { -        return new FeatureFlagRepositoryFactory().create(getApplicationContext()); -    } -      private void createProfileRecords(              AppPredictorFactory factory, IntentFilter targetIntentFilter) { -        UserHandle mainUserHandle = getPersonalProfileUserHandle(); +        UserHandle mainUserHandle = getAnnotatedUserHandles().personalProfileUserHandle;          ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);          if (record.shortcutLoader == null) {              Tracer.INSTANCE.endLaunchToShortcutTrace();          } -        UserHandle workUserHandle = getWorkProfileUserHandle(); +        UserHandle workUserHandle = getAnnotatedUserHandles().workProfileUserHandle;          if (workUserHandle != null) {              createProfileRecord(workUserHandle, targetIntentFilter, factory);          } @@ -382,7 +384,7 @@ public class ChooserActivity extends ResolverActivity implements          ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()                      ? null                      : createShortcutLoader( -                            getApplicationContext(), +                            this,                              appPredictor,                              userHandle,                              targetIntentFilter, @@ -406,7 +408,7 @@ public class ChooserActivity extends ResolverActivity implements              Consumer<ShortcutLoader.Result> callback) {          return new ShortcutLoader(                  context, -                getLifecycle(), +                getCoroutineScope(getLifecycle()),                  appPredictor,                  userHandle,                  targetIntentFilter, @@ -414,23 +416,11 @@ public class ChooserActivity extends ResolverActivity implements      }      static SharedPreferences getPinnedSharedPrefs(Context context) { -        // The code below is because in the android:ui process, no one can hear you scream. -        // The package info in the context isn't initialized in the way it is for normal apps, -        // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we -        // build the path manually below using the same policy that appears in ContextImpl. -        // This fails silently under the hood if there's a problem, so if we find ourselves in -        // the case where we don't have access to credential encrypted storage we just won't -        // have our pinned target info. -        final File prefsFile = new File(new File( -                Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL, -                        context.getUserId(), context.getPackageName()), -                "shared_prefs"), -                PINNED_SHARED_PREFS_NAME + ".xml"); -        return context.getSharedPreferences(prefsFile, MODE_PRIVATE); +        return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);      }      @Override -    protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( +    protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(              Intent[] initialIntents,              List<ResolveInfo> rList,              boolean filterLastUsed, @@ -475,9 +465,12 @@ public class ChooserActivity extends ResolverActivity implements                          /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,                          /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); -        return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), -                noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, -                createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); +        return new NoCrossProfileEmptyStateProvider( +                getAnnotatedUserHandles().personalProfileUserHandle, +                noWorkToPersonalEmptyState, +                noPersonalToWorkEmptyState, +                createCrossProfileIntentsChecker(), +                getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);      }      private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( @@ -491,7 +484,7 @@ public class ChooserActivity extends ResolverActivity implements                  initialIntents,                  rList,                  filterLastUsed, -                /* userHandle */ getPersonalProfileUserHandle(), +                /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,                  targetDataLoader);          return new ChooserMultiProfilePagerAdapter(                  /* context */ this, @@ -499,8 +492,9 @@ public class ChooserActivity extends ResolverActivity implements                  createEmptyStateProvider(/* workProfileUserHandle= */ null),                  /* workProfileQuietModeChecker= */ () -> false,                  /* workProfileUserHandle= */ null, -                getCloneProfileUserHandle(), -                mMaxTargetsPerRow); +                getAnnotatedUserHandles().cloneProfileUserHandle, +                mMaxTargetsPerRow, +                mFeatureFlags);      }      private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( @@ -515,7 +509,7 @@ public class ChooserActivity extends ResolverActivity implements                  selectedProfile == PROFILE_PERSONAL ? initialIntents : null,                  rList,                  filterLastUsed, -                /* userHandle */ getPersonalProfileUserHandle(), +                /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,                  targetDataLoader);          ChooserGridAdapter workAdapter = createChooserGridAdapter(                  /* context */ this, @@ -523,40 +517,30 @@ public class ChooserActivity extends ResolverActivity implements                  selectedProfile == PROFILE_WORK ? initialIntents : null,                  rList,                  filterLastUsed, -                /* userHandle */ getWorkProfileUserHandle(), +                /* userHandle */ getAnnotatedUserHandles().workProfileUserHandle,                  targetDataLoader);          return new ChooserMultiProfilePagerAdapter(                  /* context */ this,                  personalAdapter,                  workAdapter, -                createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()), +                createEmptyStateProvider(getAnnotatedUserHandles().workProfileUserHandle),                  () -> mWorkProfileAvailability.isQuietModeEnabled(),                  selectedProfile, -                getWorkProfileUserHandle(), -                getCloneProfileUserHandle(), -                mMaxTargetsPerRow); +                getAnnotatedUserHandles().workProfileUserHandle, +                getAnnotatedUserHandles().cloneProfileUserHandle, +                mMaxTargetsPerRow, +                mFeatureFlags);      }      private int findSelectedProfile() {          int selectedProfile = getSelectedProfileExtra();          if (selectedProfile == -1) { -            selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch()); +            selectedProfile = getProfileForUser( +                    getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);          }          return selectedProfile;      } -    @Override -    protected boolean postRebuildList(boolean rebuildCompleted) { -        updateStickyContentPreview(); -        if (shouldShowStickyContentPreview() -                || mChooserMultiProfilePagerAdapter -                        .getCurrentRootAdapter().getSystemRowCount() != 0) { -            getEventLog().logActionShareWithPreview( -                    mChooserContentPreviewUi.getPreferredContentPreview()); -        } -        return postRebuildListInternal(rebuildCompleted); -    } -      /**       * Check if the profile currently used is a work profile.       * @return true if it is work profile, false if it is parent profile (or no work profile is @@ -621,7 +605,7 @@ public class ChooserActivity extends ResolverActivity implements      }      @Override -    public void onConfigurationChanged(Configuration newConfig) { +    public void onConfigurationChanged(@NonNull Configuration newConfig) {          super.onConfigurationChanged(newConfig);          ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);          if (viewPager.isLayoutRtl()) { @@ -686,7 +670,10 @@ public class ChooserActivity extends ResolverActivity implements          ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(                  getResources(),                  getLayoutInflater(), -                parent); +                parent, +                mFeatureFlags.scrollablePreview() +                        ? findViewById(R.id.chooser_headline_row_container) +                        : null);          if (layout != null) {              adjustPreviewWidth(getResources().getConfiguration().orientation, layout); @@ -807,7 +794,9 @@ public class ChooserActivity extends ResolverActivity implements      @Override      public int getLayoutResource() { -        return R.layout.chooser_grid; +        return mFeatureFlags.scrollablePreview() +                ? R.layout.chooser_grid_scrollable_preview +                : R.layout.chooser_grid;      }      @Override // ResolverListCommunicator @@ -1030,7 +1019,7 @@ public class ChooserActivity extends ResolverActivity implements          mIsSuccessfullySelected = true;      } -    private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) { +    private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {          Intent targetIntent = targetInfo.getTargetIntent();          if (targetIntent == null) {              return; @@ -1105,7 +1094,8 @@ public class ChooserActivity extends ResolverActivity implements          ProfileRecord record = getProfileRecord(userHandle);          // We cannot use APS service when clone profile is present as APS service cannot sort          // cross profile targets as of now. -        return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor; +        return ((record == null) || (getAnnotatedUserHandles().cloneProfileUserHandle != null)) +                ? null : record.appPredictor;      }      /** @@ -1130,9 +1120,6 @@ public class ChooserActivity extends ResolverActivity implements      }      protected EventLog getEventLog() { -        if (mEventLog == null) { -            mEventLog = new EventLog(); -        }          return mEventLog;      } @@ -1156,7 +1143,7 @@ public class ChooserActivity extends ResolverActivity implements          }          @Override -        boolean isComponentFiltered(ComponentName name) { +        public boolean isComponentFiltered(ComponentName name) {              return mChooserRequest.getFilteredComponentNames().contains(name);          } @@ -1184,7 +1171,7 @@ public class ChooserActivity extends ResolverActivity implements                  createListController(userHandle),                  userHandle,                  getTargetIntent(), -                mChooserRequest, +                mChooserRequest.getReferrerFillInIntent(),                  mMaxTargetsPerRow,                  targetDataLoader); @@ -1229,7 +1216,8 @@ public class ChooserActivity extends ResolverActivity implements                  },                  chooserListAdapter,                  shouldShowContentPreview(), -                mMaxTargetsPerRow); +                mMaxTargetsPerRow, +                mFeatureFlags);      }      @VisibleForTesting @@ -1242,12 +1230,12 @@ public class ChooserActivity extends ResolverActivity implements              ResolverListController resolverListController,              UserHandle userHandle,              Intent targetIntent, -            ChooserRequestParameters chooserRequest, +            Intent referrerFillInIntent,              int maxTargetsPerRow,              TargetDataLoader targetDataLoader) {          UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() -                && userHandle.equals(getPersonalProfileUserHandle()) -                ? getCloneProfileUserHandle() : userHandle; +                && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle) +                ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;          return new ChooserListAdapter(                  context,                  payloadIntents, @@ -1257,18 +1245,19 @@ public class ChooserActivity extends ResolverActivity implements                  createListController(userHandle),                  userHandle,                  targetIntent, +                referrerFillInIntent,                  this,                  context.getPackageManager(),                  getEventLog(), -                chooserRequest,                  maxTargetsPerRow,                  initialIntentsUserSpace, -                targetDataLoader); +                targetDataLoader, +                null);      }      @Override      protected void onWorkProfileStatusUpdated() { -        UserHandle workUser = getWorkProfileUserHandle(); +        UserHandle workUser = getAnnotatedUserHandles().workProfileUserHandle;          ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);          if (record != null && record.shortcutLoader != null) {              record.shortcutLoader.reset(); @@ -1323,7 +1312,8 @@ public class ChooserActivity extends ResolverActivity implements                  new ChooserActionFactory.ActionActivityStarter() {                      @Override                      public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { -                        safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle()); +                        safelyStartActivityAsUser( +                                targetInfo, getAnnotatedUserHandles().personalProfileUserHandle);                          finish();                      } @@ -1333,11 +1323,12 @@ public class ChooserActivity extends ResolverActivity implements                          ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(                                  ChooserActivity.this, sharedElement, sharedElementName);                          safelyStartActivityAsUser( -                                targetInfo, getPersonalProfileUserHandle(), options.toBundle()); +                                targetInfo, +                                getAnnotatedUserHandles().personalProfileUserHandle, +                                options.toBundle());                          // Can't finish right away because the shared element transition may not                          // be ready to start.                          mFinishWhenStopped = true; -                      }                  },                  (status) -> { @@ -1490,7 +1481,7 @@ public class ChooserActivity extends ResolverActivity implements       * Returns {@link #PROFILE_PERSONAL}, otherwise.       **/      private int getProfileForUser(UserHandle currentUserHandle) { -        if (currentUserHandle.equals(getWorkProfileUserHandle())) { +        if (currentUserHandle.equals(getAnnotatedUserHandles().workProfileUserHandle)) {              return PROFILE_WORK;          }          // We return personal profile, as it is the default when there is no work profile, personal @@ -1510,19 +1501,21 @@ public class ChooserActivity extends ResolverActivity implements      }      @Override -    public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { +    protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {          setupScrollListener();          maybeSetupGlobalLayoutListener();          ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; -        if (chooserListAdapter.getUserHandle() -                .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { +        UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle(); +        if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {              mChooserMultiProfilePagerAdapter.getActiveAdapterView()                      .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());              mChooserMultiProfilePagerAdapter                      .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());          } +        //TODO: move this block inside ChooserListAdapter (should be called when +        // ResolverListAdapter#mPostListReadyRunnable is executed.          if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {              chooserListAdapter.notifyDataSetChanged();          } else { @@ -1530,25 +1523,28 @@ public class ChooserActivity extends ResolverActivity implements          }          if (rebuildComplete) { -            long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle()); +            long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);              if (duration >= 0) {                  Log.d(TAG, "app target loading time " + duration + " ms");              }              addCallerChooserTargets();              getEventLog().logSharesheetAppLoadComplete(); -            maybeQueryAdditionalPostProcessingTargets(chooserListAdapter); +            maybeQueryAdditionalPostProcessingTargets( +                    listProfileUserHandle, +                    chooserListAdapter.getDisplayResolveInfos());              mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);          }      } -    private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { -        UserHandle userHandle = chooserListAdapter.getUserHandle(); +    private void maybeQueryAdditionalPostProcessingTargets( +            UserHandle userHandle, +            DisplayResolveInfo[] displayResolveInfos) {          ProfileRecord record = getProfileRecord(userHandle);          if (record == null || record.shortcutLoader == null) {              return;          }          record.loadingStartTime = SystemClock.elapsedRealtime(); -        record.shortcutLoader.updateAppTargets(chooserListAdapter.getDisplayResolveInfos()); +        record.shortcutLoader.updateAppTargets(displayResolveInfos);      }      @MainThread @@ -1596,7 +1592,8 @@ public class ChooserActivity extends ResolverActivity implements                  getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);          mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(                  new RecyclerView.OnScrollListener() { -                    public void onScrollStateChanged(RecyclerView view, int scrollState) { +                    @Override +                    public void onScrollStateChanged(@NonNull RecyclerView view, int scrollState) {                          if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {                              if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {                                  mScrollStatus = SCROLL_STATUS_IDLE; @@ -1610,7 +1607,8 @@ public class ChooserActivity extends ResolverActivity implements                          }                      } -                    public void onScrolled(RecyclerView view, int dx, int dy) { +                    @Override +                    public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {                          if (view.getChildCount() > 0) {                              View child = view.getLayoutManager().findViewByPosition(0);                              if (child == null || child.getTop() < 0) { @@ -1656,11 +1654,13 @@ public class ChooserActivity extends ResolverActivity implements      }      private boolean shouldShowStickyContentPreviewNoOrientationCheck() { -        return shouldShowTabs() -                && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( -                UserHandle.of(UserHandle.myUserId())).getCount() > 0 -                || shouldShowContentPreviewWhenEmpty()) -                && shouldShowContentPreview(); +        if (!shouldShowContentPreview()) { +            return false; +        } +        boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle( +                UserHandle.of(UserHandle.myUserId())).getCount() == 0; +        return (mFeatureFlags.scrollablePreview() || shouldShowTabs()) +                && (!isEmpty || shouldShowContentPreviewWhenEmpty());      }      /** diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java index 5f373525..aaa7554c 100644 --- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java +++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java @@ -70,7 +70,7 @@ public class ChooserGridLayoutManager extends GridLayoutManager {          return super.getRowCountForAccessibility(recycler, state) - 1;      } -    void setVerticalScrollEnabled(boolean verticalScrollEnabled) { +    public void setVerticalScrollEnabled(boolean verticalScrollEnabled) {          mVerticalScrollEnabled = verticalScrollEnabled;      } diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java index 5fbf03a0..7cd86bf4 100644 --- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java +++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java @@ -16,12 +16,13 @@  package com.android.intentresolver; -import android.annotation.Nullable;  import android.content.ComponentName;  import android.content.Context;  import android.provider.Settings;  import android.text.TextUtils; +import androidx.annotation.Nullable; +  import com.android.internal.annotations.VisibleForTesting;  /** @@ -50,7 +51,8 @@ public class ChooserIntegratedDeviceComponents {      @VisibleForTesting      ChooserIntegratedDeviceComponents( -            ComponentName editSharingComponent, ComponentName nearbySharingComponent) { +            @Nullable ComponentName editSharingComponent, +            @Nullable ComponentName nearbySharingComponent) {          mEditSharingComponent = editSharingComponent;          mNearbySharingComponent = nearbySharingComponent;      } diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index e6d6dbf4..876ad5c3 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -19,7 +19,6 @@ package com.android.intentresolver;  import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;  import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; -import android.annotation.Nullable;  import android.app.ActivityManager;  import android.app.prediction.AppTarget;  import android.content.ComponentName; @@ -38,11 +37,16 @@ import android.os.UserManager;  import android.provider.DeviceConfig;  import android.service.chooser.ChooserTarget;  import android.text.Layout; +import android.text.TextUtils;  import android.util.Log;  import android.view.View;  import android.view.ViewGroup;  import android.widget.TextView; +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.MultiDisplayResolveInfo;  import com.android.intentresolver.chooser.NotSelectableTargetInfo; @@ -57,10 +61,23 @@ import java.util.ArrayList;  import java.util.HashSet;  import java.util.List;  import java.util.Map; +import java.util.Objects;  import java.util.Set; +import java.util.concurrent.Executor;  import java.util.stream.Collectors;  public class ChooserListAdapter extends ResolverListAdapter { + +    /** +     * Delegate interface for injecting a chooser-specific operation to be performed before handling +     * a package-change event. This allows the "driver" invoking the package-change to be generic, +     * with no knowledge specific to the chooser implementation. +     */ +    public interface PackageChangeCallback { +        /** Perform any steps necessary before processing the package-change event. */ +        void beforeHandlingPackagesChanged(); +    } +      private static final String TAG = "ChooserListAdapter";      private static final boolean DEBUG = false; @@ -78,13 +95,17 @@ public class ChooserListAdapter extends ResolverListAdapter {      /** {@link #getBaseScore} */      public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; -    private final ChooserRequestParameters mChooserRequest; +    private final Intent mReferrerFillInIntent; +      private final int mMaxRankedTargets;      private final EventLog mEventLog;      private final Set<TargetInfo> mRequestedIcons = new HashSet<>(); +    @Nullable +    private final PackageChangeCallback mPackageChangeCallback; +      // Reserve spots for incoming direct share targets by adding placeholders      private final TargetInfo mPlaceHolderTargetInfo;      private final TargetDataLoader mTargetDataLoader; @@ -94,7 +115,7 @@ public class ChooserListAdapter extends ResolverListAdapter {      private final ShortcutSelectionLogic mShortcutSelectionLogic;      // Sorted list of DisplayResolveInfos for the alphabetical app section. -    private List<DisplayResolveInfo> mSortedList = new ArrayList<>(); +    private final List<DisplayResolveInfo> mSortedList = new ArrayList<>();      private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker(); @@ -138,13 +159,55 @@ public class ChooserListAdapter extends ResolverListAdapter {              ResolverListController resolverListController,              UserHandle userHandle,              Intent targetIntent, +            Intent referrerFillInIntent, +            ResolverListCommunicator resolverListCommunicator, +            PackageManager packageManager, +            EventLog eventLog, +            int maxRankedTargets, +            UserHandle initialIntentsUserSpace, +            TargetDataLoader targetDataLoader, +            @Nullable PackageChangeCallback packageChangeCallback) { +        this( +                context, +                payloadIntents, +                initialIntents, +                rList, +                filterLastUsed, +                resolverListController, +                userHandle, +                targetIntent, +                referrerFillInIntent, +                resolverListCommunicator, +                packageManager, +                eventLog, +                maxRankedTargets, +                initialIntentsUserSpace, +                targetDataLoader, +                packageChangeCallback, +                AsyncTask.SERIAL_EXECUTOR, +                context.getMainExecutor()); +    } + +    @VisibleForTesting +    public ChooserListAdapter( +            Context context, +            List<Intent> payloadIntents, +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            ResolverListController resolverListController, +            UserHandle userHandle, +            Intent targetIntent, +            Intent referrerFillInIntent,              ResolverListCommunicator resolverListCommunicator,              PackageManager packageManager,              EventLog eventLog, -            ChooserRequestParameters chooserRequest,              int maxRankedTargets,              UserHandle initialIntentsUserSpace, -            TargetDataLoader targetDataLoader) { +            TargetDataLoader targetDataLoader, +            @Nullable PackageChangeCallback packageChangeCallback, +            Executor bgExecutor, +            Executor mainExecutor) {          // Don't send the initial intents through the shared ResolverActivity path,          // we want to separate them into a different section.          super( @@ -158,13 +221,16 @@ public class ChooserListAdapter extends ResolverListAdapter {                  targetIntent,                  resolverListCommunicator,                  initialIntentsUserSpace, -                targetDataLoader); +                targetDataLoader, +                bgExecutor, +                mainExecutor); -        mChooserRequest = chooserRequest;          mMaxRankedTargets = maxRankedTargets; +        mReferrerFillInIntent = referrerFillInIntent;          mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);          mTargetDataLoader = targetDataLoader; +        mPackageChangeCallback = packageChangeCallback;          createPlaceHolders();          mEventLog = eventLog;          mShortcutSelectionLogic = new ShortcutSelectionLogic( @@ -227,9 +293,8 @@ public class ChooserListAdapter extends ResolverListAdapter {                      ri.icon = 0;                  }                  ri.userHandle = initialIntentsUserSpace; -                // TODO: remove DisplayResolveInfo dependency on presentation getter -                DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( -                        ii, ri, ii, mTargetDataLoader.createPresentationGetter(ri)); +                DisplayResolveInfo displayResolveInfo = +                        DisplayResolveInfo.newDisplayResolveInfo(ii, ri, ii);                  mCallerTargets.add(displayResolveInfo);                  if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;              } @@ -238,6 +303,9 @@ public class ChooserListAdapter extends ResolverListAdapter {      @Override      public void handlePackagesChanged() { +        if (mPackageChangeCallback != null) { +            mPackageChangeCallback.beforeHandlingPackagesChanged(); +        }          if (DEBUG) {              Log.d(TAG, "clearing queryTargets on package change");          } @@ -247,7 +315,7 @@ public class ChooserListAdapter extends ResolverListAdapter {      }      @Override -    protected boolean rebuildList(boolean doPostProcessing) { +    public boolean rebuildList(boolean doPostProcessing) {          mAnimationTracker.reset();          mSortedList.clear();          boolean result = super.rebuildList(doPostProcessing); @@ -272,75 +340,77 @@ public class ChooserListAdapter extends ResolverListAdapter {      public void onBindView(View view, TargetInfo info, int position) {          final ViewHolder holder = (ViewHolder) view.getTag(); +        holder.reset(); +        // Always remove the spacing listener, attach as needed to direct share targets below. +        holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); +          if (info == null) {              holder.icon.setImageDrawable(loadIconPlaceholder());              return;          } -        holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo()); -        mAnimationTracker.animateLabel(holder.text, info); -        if (holder.text2.getVisibility() == View.VISIBLE) { +        final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), ""); +        final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), ""); +        holder.bindLabel(displayLabel, extendedInfo); +        if (!TextUtils.isEmpty(displayLabel)) { +            mAnimationTracker.animateLabel(holder.text, info); +        } +        if (!TextUtils.isEmpty(extendedInfo) && holder.text2.getVisibility() == View.VISIBLE) {              mAnimationTracker.animateLabel(holder.text2, info);          } +          holder.bindIcon(info); -        if (info.getDisplayIconHolder().getDisplayIcon() != null) { +        if (info.hasDisplayIcon()) {              mAnimationTracker.animateIcon(holder.icon, info); -        } else { -            holder.icon.clearAnimation();          }          if (info.isSelectableTargetInfo()) {              // direct share targets should append the application name for a better readout              DisplayResolveInfo rInfo = info.getDisplayResolveInfo(); -            CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; -            CharSequence extendedInfo = info.getExtendedInfo(); -            String contentDescription = String.join(" ", info.getDisplayLabel(), -                    extendedInfo != null ? extendedInfo : "", appName); +            CharSequence appName = +                    Objects.requireNonNullElse(rInfo == null ? null : rInfo.getDisplayLabel(), ""); +            String contentDescription = +                    String.join(" ", info.getDisplayLabel(), extendedInfo, appName); +            if (info.isPinned()) { +                contentDescription = String.join( +                    ". ", +                    contentDescription, +                    mContext.getResources().getString(R.string.pinned)); +            }              holder.updateContentDescription(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))); +            }              DisplayResolveInfo dri = (DisplayResolveInfo) info;              if (!dri.hasDisplayIcon()) {                  loadIcon(dri);              } +            if (!dri.hasDisplayLabel()) { +                loadLabel(dri); +            }          } -        // If target is loading, show a special placeholder shape in the label, make unclickable          if (info.isPlaceHolderTargetInfo()) { -            final int maxWidth = mContext.getResources().getDimensionPixelSize( -                    R.dimen.chooser_direct_share_label_placeholder_max_width); -            holder.text.setMaxWidth(maxWidth); -            holder.text.setBackground(mContext.getResources().getDrawable( -                    R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme())); -            // Prevent rippling by removing background containing ripple -            holder.itemView.setBackground(null); -        } else { -            holder.text.setMaxWidth(Integer.MAX_VALUE); -            holder.text.setBackground(null); -            holder.itemView.setBackground(holder.defaultItemViewBackground); +            holder.bindPlaceholder();          } -        // Always remove the spacing listener, attach as needed to direct share targets below. -        holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); -          if (info.isMultiDisplayResolveInfo()) {              // If the target is grouped show an indicator -            Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background); -            holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0); -            holder.text.setBackground(bkg); +            holder.bindGroupIndicator( +                    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 -            Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background); -            holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0); -            holder.text.setBackground(bkg); +            holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background));              holder.text.addOnLayoutChangeListener(mPinTextSpacingListener); -        } else { -            holder.text.setBackground(null); -            holder.text.setPaddingRelative(0, 0, 0, 0);          }      } @@ -360,9 +430,13 @@ public class ChooserListAdapter extends ResolverListAdapter {          }      } -    void updateAlphabeticalList() { -        // TODO: this procedure seems like it should be relatively lightweight. Why does it need to -        // run in an `AsyncTask`? +    public void updateAlphabeticalList() { +        final ChooserActivity.AzInfoComparator comparator = +                new ChooserActivity.AzInfoComparator(mContext); +        final List<DisplayResolveInfo> allTargets = new ArrayList<>(); +        allTargets.addAll(getTargetsInCurrentDisplayList()); +        allTargets.addAll(mCallerTargets); +          new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {              @Override              protected List<DisplayResolveInfo> doInBackground(Void... voids) { @@ -375,32 +449,39 @@ public class ChooserListAdapter extends ResolverListAdapter {              }              private List<DisplayResolveInfo> updateList() { -                List<DisplayResolveInfo> allTargets = new ArrayList<>(); -                allTargets.addAll(getTargetsInCurrentDisplayList()); -                allTargets.addAll(mCallerTargets); +                loadMissingLabels(allTargets);                  // Consolidate multiple targets from same app.                  return allTargets                          .stream()                          .collect(Collectors.groupingBy(target ->                                  target.getResolvedComponentName().getPackageName() -                                + "#" + target.getDisplayLabel() -                                + '#' + target.getResolveInfo().userHandle.getIdentifier() +                                        + "#" + target.getDisplayLabel() +                                        + '#' + target.getResolveInfo().userHandle.getIdentifier()                          ))                          .values()                          .stream()                          .map(appTargets ->                                  (appTargets.size() == 1) -                                ? appTargets.get(0) -                                : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(appTargets)) -                        .sorted(new ChooserActivity.AzInfoComparator(mContext)) +                                        ? appTargets.get(0) +                                        : MultiDisplayResolveInfo.newMultiDisplayResolveInfo( +                                            appTargets)) +                        .sorted(comparator)                          .collect(Collectors.toList());              } +              @Override              protected void onPostExecute(List<DisplayResolveInfo> newList) { -                mSortedList = newList; +                mSortedList.clear(); +                mSortedList.addAll(newList);                  notifyDataSetChanged();              } + +            private void loadMissingLabels(List<DisplayResolveInfo> targets) { +                for (DisplayResolveInfo target: targets) { +                    mTargetDataLoader.getOrLoadLabel(target); +                } +            }          }.execute();      } @@ -438,8 +519,14 @@ public class ChooserListAdapter extends ResolverListAdapter {          return count;      } +    private static boolean hasSendAction(Intent intent) { +        String action = intent.getAction(); +        return Intent.ACTION_SEND.equals(action) +                || Intent.ACTION_SEND_MULTIPLE.equals(action); +    } +      public int getServiceTargetCount() { -        if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) { +        if (hasSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {              return Math.min(mServiceTargets.size(), mMaxRankedTargets);          } @@ -553,7 +640,7 @@ public class ChooserListAdapter extends ResolverListAdapter {      protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {          // Checks if this info is already listed in callerTargets.          for (TargetInfo existingInfo : mCallerTargets) { -            if (mResolverListCommunicator.resolveInfoMatch( +            if (ResolveInfoHelpers.resolveInfoMatch(                      dri.getResolveInfo(), existingInfo.getResolveInfo())) {                  return false;              } @@ -594,8 +681,8 @@ public class ChooserListAdapter extends ResolverListAdapter {                  directShareToShortcutInfos,                  directShareToAppTargets,                  mContext.createContextAsUser(getUserHandle(), 0), -                mChooserRequest.getTargetIntent(), -                mChooserRequest.getReferrerFillInIntent(), +                getTargetIntent(), +                mReferrerFillInIntent,                  mMaxRankedTargets,                  mServiceTargets);          if (isUpdated) { @@ -644,29 +731,23 @@ public class ChooserListAdapter extends ResolverListAdapter {       * in the head of input list and fill the tail with other elements in undetermined order.       */      @Override -    AsyncTask<List<ResolvedComponentInfo>, -                Void, -                List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { -        return new AsyncTask<List<ResolvedComponentInfo>, -                Void, -                List<ResolvedComponentInfo>>() { -            @Override -            protected List<ResolvedComponentInfo> doInBackground( -                    List<ResolvedComponentInfo>... params) { -                Trace.beginSection("ChooserListAdapter#SortingTask"); -                mResolverListController.topK(params[0], mMaxRankedTargets); -                Trace.endSection(); -                return params[0]; -            } -            @Override -            protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { -                processSortedList(sortedComponents, doPostProcessing); -                if (doPostProcessing) { -                    mResolverListCommunicator.updateProfileViewButton(); -                    notifyDataSetChanged(); -                } -            } -        }; +    @WorkerThread +    protected void sortComponents(List<ResolvedComponentInfo> components) { +        Trace.beginSection("ChooserListAdapter#SortingTask"); +        mResolverListController.topK(components, mMaxRankedTargets); +        Trace.endSection();      } +    @Override +    @MainThread +    protected void onComponentsSorted( +            @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/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index c159243e..080f9d24 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager;  import androidx.recyclerview.widget.RecyclerView;  import androidx.viewpager.widget.PagerAdapter; +import com.android.intentresolver.emptystate.EmptyStateProvider;  import com.android.intentresolver.grid.ChooserGridAdapter;  import com.android.intentresolver.measurements.Tracer;  import com.android.internal.annotations.VisibleForTesting; @@ -38,21 +39,22 @@ import java.util.function.Supplier;   * A {@link PagerAdapter} which describes the work and personal profile share sheet screens.   */  @VisibleForTesting -public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter< +public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<          RecyclerView, ChooserGridAdapter, ChooserListAdapter> {      private static final int SINGLE_CELL_SPAN_SIZE = 1;      private final ChooserProfileAdapterBinder mAdapterBinder;      private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; -    ChooserMultiProfilePagerAdapter( +    public ChooserMultiProfilePagerAdapter(              Context context,              ChooserGridAdapter adapter,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle, -            int maxTargetsPerRow) { +            int maxTargetsPerRow, +            FeatureFlags featureFlags) {          this(                  context,                  new ChooserProfileAdapterBinder(maxTargetsPerRow), @@ -62,10 +64,11 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda                  /* defaultProfile= */ 0,                  workProfileUserHandle,                  cloneProfileUserHandle, -                new BottomPaddingOverrideSupplier(context)); +                new BottomPaddingOverrideSupplier(context), +                featureFlags);      } -    ChooserMultiProfilePagerAdapter( +    public ChooserMultiProfilePagerAdapter(              Context context,              ChooserGridAdapter personalAdapter,              ChooserGridAdapter workAdapter, @@ -74,7 +77,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda              @Profile int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle, -            int maxTargetsPerRow) { +            int maxTargetsPerRow, +            FeatureFlags featureFlags) {          this(                  context,                  new ChooserProfileAdapterBinder(maxTargetsPerRow), @@ -84,7 +88,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda                  defaultProfile,                  workProfileUserHandle,                  cloneProfileUserHandle, -                new BottomPaddingOverrideSupplier(context)); +                new BottomPaddingOverrideSupplier(context), +                featureFlags);      }      private ChooserMultiProfilePagerAdapter( @@ -96,9 +101,9 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda              @Profile int defaultProfile,              UserHandle workProfileUserHandle,              UserHandle cloneProfileUserHandle, -            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { +            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, +            FeatureFlags featureFlags) {          super( -                context,                          gridAdapter -> gridAdapter.getListAdapter(),                  adapterBinder,                  gridAdapters, @@ -107,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda                  defaultProfile,                  workProfileUserHandle,                  cloneProfileUserHandle, -                        () -> makeProfileView(context), +                        () -> makeProfileView(context, featureFlags),                  bottomPaddingOverrideSupplier);          mAdapterBinder = adapterBinder;          mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; @@ -131,10 +136,12 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda          }      } -    private static ViewGroup makeProfileView(Context context) { +    private static ViewGroup makeProfileView( +            Context context, FeatureFlags featureFlags) {          LayoutInflater inflater = LayoutInflater.from(context); -        ViewGroup rootView = (ViewGroup) inflater.inflate( -                R.layout.chooser_list_per_profile, null, false); +        ViewGroup rootView = featureFlags.scrollablePreview() +                ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) +                : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);          RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);          recyclerView.setAccessibilityDelegateCompat(                  new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); @@ -142,7 +149,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda      }      @Override -    boolean rebuildActiveTab(boolean doPostProcessing) { +    public boolean rebuildActiveTab(boolean doPostProcessing) {          if (doPostProcessing) {              Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle());          } @@ -150,7 +157,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda      }      @Override -    boolean rebuildInactiveTab(boolean doPostProcessing) { +    public boolean rebuildInactiveTab(boolean doPostProcessing) {          if (getItemCount() != 1 && doPostProcessing) {              Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle());          } diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java index 250b6827..d6688d90 100644 --- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java +++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java @@ -16,20 +16,20 @@  package com.android.intentresolver; -import android.annotation.NonNull;  import android.graphics.Rect;  import android.view.View;  import android.view.ViewGroup;  import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.NonNull;  import androidx.recyclerview.widget.RecyclerView;  import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; -class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate { +public class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {      private final Rect mTempRect = new Rect();      private final int[] mConsumed = new int[2]; -    ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) { +    public ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {          super(recyclerView);      } diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java index 2ebe48a6..474b240f 100644 --- a/java/src/com/android/intentresolver/ChooserRefinementManager.java +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -16,8 +16,6 @@  package com.android.intentresolver; -import android.annotation.Nullable; -import android.annotation.UiThread;  import android.app.Activity;  import android.app.Application;  import android.content.Intent; @@ -28,22 +26,30 @@ import android.os.Parcel;  import android.os.ResultReceiver;  import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread;  import androidx.lifecycle.LiveData;  import androidx.lifecycle.MutableLiveData;  import androidx.lifecycle.ViewModel;  import com.android.intentresolver.chooser.TargetInfo; +import dagger.hilt.android.lifecycle.HiltViewModel; +  import java.util.List;  import java.util.function.Consumer; +import javax.inject.Inject; + +  /**   * Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement   * activity" that will be invoked when a target is selected, allowing the calling app to add - * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to + * additional extras and other refinements (subject to {@link Intent#filterEquals}), e.g., to   * convert the format of the payload, or lazy-download some data that was deferred in the original   * call).   */ +@HiltViewModel  @UiThread  public final class ChooserRefinementManager extends ViewModel {      private static final String TAG = "ChooserRefinement"; @@ -88,6 +94,9 @@ public final class ChooserRefinementManager extends ViewModel {      private MutableLiveData<RefinementCompletion> mRefinementCompletion = new MutableLiveData<>(); +    @Inject +    public ChooserRefinementManager() {} +      public LiveData<RefinementCompletion> getRefinementCompletion() {          return mRefinementCompletion;      } diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 5157986b..7ad809e9 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -16,8 +16,6 @@  package com.android.intentresolver; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.content.ComponentName;  import android.content.Intent;  import android.content.IntentFilter; @@ -32,7 +30,9 @@ import android.text.TextUtils;  import android.util.Log;  import android.util.Pair; -import com.android.intentresolver.flags.FeatureFlagRepository; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +  import com.android.intentresolver.util.UriFilters;  import com.google.common.collect.ImmutableList; @@ -104,8 +104,7 @@ public class ChooserRequestParameters {      public ChooserRequestParameters(              final Intent clientIntent,              String referrerPackageName, -            final Uri referrer, -            FeatureFlagRepository featureFlags) { +            final Uri referrer) {          final Intent requestedTarget = parseTargetIntentExtra(                  clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));          mTarget = intentWithModifiedLaunchFlags(requestedTarget); @@ -212,7 +211,7 @@ public class ChooserRequestParameters {      /**       * TODO: this returns a nullable array for convenience, but if the legacy APIs can be -     * refactored, returning {@link mAdditionalTargets} directly is simpler and safer. +     * refactored, returning {@link #mAdditionalTargets} directly is simpler and safer.       */      @Nullable      public Intent[] getAdditionalTargets() { @@ -226,7 +225,7 @@ public class ChooserRequestParameters {      /**       * TODO: this returns a nullable array for convenience, but if the legacy APIs can be -     * refactored, returning {@link mInitialIntents} directly is simpler and safer. +     * refactored, returning {@link #mInitialIntents} directly is simpler and safer.       */      @Nullable      public Intent[] getInitialIntents() { @@ -288,7 +287,7 @@ public class ChooserRequestParameters {       * requested target <em>wasn't</em> a send action; otherwise it is null. The second value is       * the resource ID of a default title string; this is nonzero only if the first value is null.       * -     * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or +     * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate}, or       * create a real type (not {@link Pair}) to express the semantics described in this comment.       */      private static Pair<CharSequence, Integer> makeTitleSpec( @@ -371,7 +370,7 @@ public class ChooserRequestParameters {       * the required type. If false, throw an {@link IllegalArgumentException} if the extra is       * non-null but can't be assigned to variables of type {@code T}.       * @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't -     * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true). +     * present in the intent (or if it had the wrong type, but <em>warnOnTypeError</em> is true).       * If false, return null in these cases, and only return an empty stream if the intent       * explicitly provided an empty array for the specified extra.       */ diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java index 2cfceeae..f0fcd149 100644 --- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager;  import android.graphics.drawable.Drawable;  import android.os.UserHandle; +import androidx.annotation.NonNull;  import androidx.fragment.app.FragmentManager;  import com.android.intentresolver.chooser.DisplayResolveInfo; @@ -66,6 +67,7 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF          dismiss();      } +    @NonNull      @Override      protected CharSequence getItemLabel(DisplayResolveInfo dri) {          final PackageManager pm = getContext().getPackageManager(); diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java index 4bfb21aa..b6b7de96 100644 --- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java @@ -21,8 +21,6 @@ import static android.content.Context.ACTIVITY_SERVICE;  import static java.util.stream.Collectors.toList; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.ActivityManager;  import android.app.Dialog;  import android.content.ComponentName; @@ -46,6 +44,8 @@ import android.view.ViewGroup;  import android.widget.ImageView;  import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import androidx.fragment.app.DialogFragment;  import androidx.fragment.app.FragmentManager;  import androidx.recyclerview.widget.RecyclerView; diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java deleted file mode 100644 index a1c53402..00000000 --- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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; - -import android.annotation.Nullable; -import android.content.Context; -import android.os.UserHandle; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import com.android.internal.annotations.VisibleForTesting; - -import com.google.common.collect.ImmutableList; - -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Implementation of {@link AbstractMultiProfilePagerAdapter} that consolidates the variation in - * existing implementations; most overrides were only to vary type signatures (which are better - * represented via generic types), and a few minor behavioral customizations are now implemented - * through small injectable delegate classes. - * TODO: now that the existing implementations are shown to be expressible in terms of this new - * generic type, merge up into the base class and simplify the public APIs. - * TODO: attempt to further restrict visibility in the methods we expose. - * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive" - * 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. - * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?) - * - * @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 - * the per-profile records. - * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to - * control the contents of a given per-profile list. This is provided for convenience, since it must - * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}. - * - * TODO: this class doesn't make any explicit usage of the {@link ResolverListAdapter} API, so the - * type constraint can probably be dropped once the API is merged upwards and cleaned. - */ -class GenericMultiProfilePagerAdapter< -        PageViewT extends ViewGroup, -        SinglePageAdapterT, -        ListAdapterT extends ResolverListAdapter> extends AbstractMultiProfilePagerAdapter { - -    /** Delegate to set up a given adapter and page view to be used together. */ -    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); -    } - -    private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor; -    private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder; -    private final Supplier<ViewGroup> mPageViewInflater; -    private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier; - -    private final ImmutableList<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems; - -    GenericMultiProfilePagerAdapter( -            Context context, -            Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor, -            AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, -            ImmutableList<SinglePageAdapterT> adapters, -            EmptyStateProvider emptyStateProvider, -            Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, -            UserHandle workProfileUserHandle, -            UserHandle cloneProfileUserHandle, -            Supplier<ViewGroup> pageViewInflater, -            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { -        super( -                context, -                /* currentPage= */ defaultProfile, -                emptyStateProvider, -                workProfileQuietModeChecker, -                workProfileUserHandle, -                cloneProfileUserHandle); - -        mListAdapterExtractor = listAdapterExtractor; -        mAdapterBinder = adapterBinder; -        mPageViewInflater = pageViewInflater; -        mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; - -        ImmutableList.Builder<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> items = -                new ImmutableList.Builder<>(); -        for (SinglePageAdapterT adapter : adapters) { -            items.add(createProfileDescriptor(adapter)); -        } -        mItems = items.build(); -    } - -    private GenericProfileDescriptor<PageViewT, SinglePageAdapterT> -            createProfileDescriptor(SinglePageAdapterT adapter) { -        return new GenericProfileDescriptor<>(mPageViewInflater.get(), adapter); -    } - -    @Override -    protected GenericProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) { -        return mItems.get(pageIndex); -    } - -    @Override -    public int getItemCount() { -        return mItems.size(); -    } - -    public PageViewT getListViewForIndex(int index) { -        return getItem(index).mView; -    } - -    @Override -    @VisibleForTesting -    public SinglePageAdapterT getAdapterForIndex(int index) { -        return getItem(index).mAdapter; -    } - -    @Override -    protected void setupListAdapter(int pageIndex) { -        mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex)); -    } - -    @Override -    public ViewGroup instantiateItem(ViewGroup container, int position) { -        setupListAdapter(position); -        return super.instantiateItem(container, position); -    } - -    @Override -    @Nullable -    protected 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; -    } - -    @Override -    @VisibleForTesting -    public ListAdapterT getActiveListAdapter() { -        return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage())); -    } - -    @Override -    @VisibleForTesting -    public ListAdapterT getInactiveListAdapter() { -        if (getCount() < 2) { -            return null; -        } -        return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage())); -    } - -    @Override -    public ListAdapterT getPersonalListAdapter() { -        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL)); -    } - -    @Override -    public ListAdapterT getWorkListAdapter() { -        if (!hasAdapterForIndex(PROFILE_WORK)) { -            return null; -        } -        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); -    } - -    @Override -    protected SinglePageAdapterT getCurrentRootAdapter() { -        return getAdapterForIndex(getCurrentPage()); -    } - -    @Override -    protected PageViewT getActiveAdapterView() { -        return getListViewForIndex(getCurrentPage()); -    } - -    @Override -    protected PageViewT getInactiveAdapterView() { -        if (getCount() < 2) { -            return null; -        } -        return getListViewForIndex(1 - getCurrentPage()); -    } - -    @Override -    protected void setupContainerPadding(View container) { -        Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); -        bottomPaddingOverride.ifPresent(paddingBottom -> -                container.setPadding( -                    container.getPaddingLeft(), -                    container.getPaddingTop(), -                    container.getPaddingRight(), -                    paddingBottom)); -    } - -    private boolean hasAdapterForIndex(int pageIndex) { -        return (pageIndex < getCount()); -    } - -    // 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 GenericProfileDescriptor<PageViewT, SinglePageAdapterT> extends -            ProfileDescriptor { -        private final SinglePageAdapterT mAdapter; -        private final PageViewT mView; - -        GenericProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) { -            super(rootView); -            mAdapter = adapter; -            mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); -        } -    } -} diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java index 5e8945f1..15996d00 100644 --- a/java/src/com/android/intentresolver/IntentForwarderActivity.java +++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java @@ -23,7 +23,6 @@ import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;  import static com.android.intentresolver.ResolverActivity.EXTRA_CALLING_USER;  import static com.android.intentresolver.ResolverActivity.EXTRA_SELECTED_PROFILE; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.ActivityThread;  import android.app.AppGlobals; @@ -45,6 +44,8 @@ import android.provider.Settings;  import android.util.Slog;  import android.widget.Toast; +import androidx.annotation.Nullable; +  import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -309,7 +310,7 @@ public class IntentForwarderActivity extends Activity  {       * Check whether the intent can be forwarded to target user. Return the intent used for       * forwarding if it can be forwarded, {@code null} otherwise.       */ -    static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId, +    public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,              IPackageManager packageManager, ContentResolver contentResolver)  {          Intent forwardIntent = new Intent(incomingIntent);          forwardIntent.addFlags( diff --git a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt b/java/src/com/android/intentresolver/MainApplication.kt index 1ddf7462..0a826629 100644 --- a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt +++ b/java/src/com/android/intentresolver/MainApplication.kt @@ -16,8 +16,7 @@  package com.android.intentresolver -/** - * Specifies expected feature flag values for a test. - */ -@Target(AnnotationTarget.FUNCTION) -annotation class RequireFeatureFlags(val flags: Array<String>, val values: BooleanArray) +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp(Application::class) open class MainApplication : Hilt_MainApplication() diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java index 4b06db3b..42a29e55 100644 --- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java @@ -15,15 +15,6 @@   */  package com.android.intentresolver; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.app.AppGlobals; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.IPackageManager;  import android.os.Trace;  import android.os.UserHandle;  import android.view.View; @@ -31,62 +22,124 @@ import android.view.ViewGroup;  import android.widget.Button;  import android.widget.TextView; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import androidx.viewpager.widget.PagerAdapter;  import androidx.viewpager.widget.ViewPager; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.emptystate.EmptyStateUiHelper;  import com.android.internal.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +  import java.util.HashSet; -import java.util.List; -import java.util.Objects; +import java.util.Optional;  import java.util.Set; +import java.util.function.Function;  import java.util.function.Supplier;  /** - * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for - * intent resolution (including share sheet). + * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet). + * + * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose. + * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive" + * 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. + * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?) + * + * @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 + * the per-profile records. + * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to + * control the contents of a given per-profile list. This is provided for convenience, since it must + * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}. + * + * 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.   */ -public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { +public class MultiProfilePagerAdapter< +        PageViewT extends ViewGroup, +        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); +    } -    private static final String TAG = "AbstractMultiProfilePagerAdapter"; -    static final int PROFILE_PERSONAL = 0; -    static final int PROFILE_WORK = 1; +    public static final int PROFILE_PERSONAL = 0; +    public static final int PROFILE_WORK = 1;      @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) -    @interface Profile {} +    public @interface Profile {} -    private final Context mContext; -    private int mCurrentPage; -    private OnProfileSelectedListener mOnProfileSelectedListener; +    private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor; +    private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder; +    private final Supplier<ViewGroup> mPageViewInflater; +    private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier; + +    private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems; -    private Set<Integer> mLoadedPages;      private final EmptyStateProvider mEmptyStateProvider;      private final UserHandle mWorkProfileUserHandle;      private final UserHandle mCloneProfileUserHandle;      private final Supplier<Boolean> mWorkProfileQuietModeChecker;  // True when work is quiet. -    AbstractMultiProfilePagerAdapter( -            Context context, -            int currentPage, +    private Set<Integer> mLoadedPages; +    private int mCurrentPage; +    private OnProfileSelectedListener mOnProfileSelectedListener; + +    protected MultiProfilePagerAdapter( +            Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor, +            AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, +            ImmutableList<SinglePageAdapterT> adapters,              EmptyStateProvider emptyStateProvider,              Supplier<Boolean> workProfileQuietModeChecker, +            @Profile int defaultProfile,              UserHandle workProfileUserHandle, -            UserHandle cloneProfileUserHandle) { -        mContext = Objects.requireNonNull(context); -        mCurrentPage = currentPage; +            UserHandle cloneProfileUserHandle, +            Supplier<ViewGroup> pageViewInflater, +            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { +        mCurrentPage = defaultProfile;          mLoadedPages = new HashSet<>();          mWorkProfileUserHandle = workProfileUserHandle;          mCloneProfileUserHandle = cloneProfileUserHandle;          mEmptyStateProvider = emptyStateProvider;          mWorkProfileQuietModeChecker = workProfileQuietModeChecker; + +        mListAdapterExtractor = listAdapterExtractor; +        mAdapterBinder = adapterBinder; +        mPageViewInflater = pageViewInflater; +        mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; + +        ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items = +                new ImmutableList.Builder<>(); +        for (SinglePageAdapterT adapter : adapters) { +            items.add(createProfileDescriptor(adapter)); +        } +        mItems = items.build();      } -    void setOnProfileSelectedListener(OnProfileSelectedListener listener) { -        mOnProfileSelectedListener = listener; +    private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor( +            SinglePageAdapterT adapter) { +        return new ProfileDescriptor<>(mPageViewInflater.get(), adapter);      } -    Context getContext() { -        return mContext; +    public void setOnProfileSelectedListener(OnProfileSelectedListener listener) { +        mOnProfileSelectedListener = listener;      }      /** @@ -94,7 +147,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed       * page and rebuilds the list.       */ -    void setupViewPager(ViewPager viewPager) { +    public void setupViewPager(ViewPager viewPager) {          viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {              @Override              public void onPageSelected(int position) { @@ -120,22 +173,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          mLoadedPages.add(mCurrentPage);      } -    void clearInactiveProfileCache() { +    public void clearInactiveProfileCache() {          if (mLoadedPages.size() == 1) {              return;          }          mLoadedPages.remove(1 - mCurrentPage);      } +    @NonNull      @Override -    public ViewGroup instantiateItem(ViewGroup container, int position) { -        final ProfileDescriptor profileDescriptor = getItem(position); -        container.addView(profileDescriptor.rootView); -        return profileDescriptor.rootView; +    public final ViewGroup instantiateItem(ViewGroup container, int position) { +        setupListAdapter(position); +        final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position); +        container.addView(descriptor.mRootView); +        return descriptor.mRootView;      }      @Override -    public void destroyItem(ViewGroup container, int position, Object view) { +    public void destroyItem(ViewGroup container, int position, @NonNull Object view) {          container.removeView((View) view);      } @@ -144,7 +199,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          return getItemCount();      } -    protected int getCurrentPage() { +    public int getCurrentPage() {          return mCurrentPage;      } @@ -154,7 +209,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {      }      @Override -    public boolean isViewFromObject(View view, Object object) { +    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {          return view == object;      } @@ -177,9 +232,11 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>       * </ul>       */ -    abstract ProfileDescriptor getItem(int pageIndex); +    private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) { +        return mItems.get(pageIndex); +    } -    protected ViewGroup getEmptyStateView(int pageIndex) { +    public ViewGroup getEmptyStateView(int pageIndex) {          return getItem(pageIndex).getEmptyStateView();      } @@ -188,13 +245,13 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * <p>For a normal consumer device with only one user returns <code>1</code>.       * <p>For a device with a work profile returns <code>2</code>.       */ -    abstract int getItemCount(); +    public final int getItemCount() { +        return mItems.size(); +    } -    /** -     * Performs view-related initialization procedures for the adapter specified -     * by <code>pageIndex</code>. -     */ -    abstract void setupListAdapter(int pageIndex); +    public final PageViewT getListViewForIndex(int index) { +        return getItem(index).mView; +    }      /**       * Returns the adapter of the list view for the relevant page specified by @@ -203,54 +260,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * depending on the adapter type.       */      @VisibleForTesting -    public abstract Object getAdapterForIndex(int pageIndex); +    public final SinglePageAdapterT getAdapterForIndex(int index) { +        return getItem(index).mAdapter; +    }      /** -     * Returns the {@link ResolverListAdapter} instance of the profile that represents +     * Performs view-related initialization procedures for the adapter specified +     * 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 ResolverListAdapter}. +     * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.       */      @Nullable -    abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle); +    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; +    }      /** -     * Returns the {@link ResolverListAdapter} instance of the profile that is currently visible +     * Returns the {@link ListAdapterT} instance of the profile that is currently visible       * 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 ResolverListAdapter}. +     * the work profile {@link ListAdapterT}.       * @see #getInactiveListAdapter()       */      @VisibleForTesting -    public abstract ResolverListAdapter getActiveListAdapter(); +    public final ListAdapterT getActiveListAdapter() { +        return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage())); +    }      /** -     * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance +     * 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 ResolverListAdapter}. +     * the personal profile {@link ListAdapterT}.       * @see #getActiveListAdapter()       */      @VisibleForTesting -    public abstract @Nullable ResolverListAdapter getInactiveListAdapter(); +    @Nullable +    public final ListAdapterT getInactiveListAdapter() { +        if (getCount() < 2) { +            return null; +        } +        return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage())); +    } -    public abstract ResolverListAdapter getPersonalListAdapter(); +    public final ListAdapterT getPersonalListAdapter() { +        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL)); +    } -    public abstract @Nullable ResolverListAdapter getWorkListAdapter(); +    @Nullable +    public final ListAdapterT getWorkListAdapter() { +        if (!hasAdapterForIndex(PROFILE_WORK)) { +            return null; +        } +        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); +    } -    abstract Object getCurrentRootAdapter(); +    public final SinglePageAdapterT getCurrentRootAdapter() { +        return getAdapterForIndex(getCurrentPage()); +    } -    abstract ViewGroup getActiveAdapterView(); +    public final PageViewT getActiveAdapterView() { +        return getListViewForIndex(getCurrentPage()); +    } -    abstract @Nullable ViewGroup getInactiveAdapterView(); +    @Nullable +    public final PageViewT getInactiveAdapterView() { +        if (getCount() < 2) { +            return null; +        } +        return getListViewForIndex(1 - getCurrentPage()); +    }      /**       * Rebuilds the tab that is currently visible to the user.       * <p>Returns {@code true} if rebuild has completed.       */ -    boolean rebuildActiveTab(boolean doPostProcessing) { +    public boolean rebuildActiveTab(boolean doPostProcessing) {          Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");          boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);          Trace.endSection(); @@ -261,7 +363,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * Rebuilds the tab that is not currently visible to the user, if such one exists.       * <p>Returns {@code true} if rebuild has completed.       */ -    boolean rebuildInactiveTab(boolean doPostProcessing) { +    public boolean rebuildInactiveTab(boolean doPostProcessing) {          Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");          if (getItemCount() == 1) {              Trace.endSection(); @@ -280,7 +382,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          }      } -    private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) { +    private boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {          if (shouldSkipRebuild(activeListAdapter)) {              activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);              return false; @@ -288,16 +390,20 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          return activeListAdapter.rebuildList(doPostProcessing);      } -    private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) { +    private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {          EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);          return emptyState != null && emptyState.shouldSkipDataRebuild();      } +    private boolean hasAdapterForIndex(int pageIndex) { +        return (pageIndex < getCount()); +    } +      /**       * The empty state screens are shown according to their priority:       * <ol>       * <li>(highest priority) cross-profile disabled by policy (handled in -     * {@link #rebuildTab(ResolverListAdapter, boolean)})</li> +     * {@link #rebuildTab(ListAdapterT, boolean)})</li>       * <li>no apps available</li>       * <li>(least priority) work is off</li>       * </ol> @@ -306,7 +412,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * the work profile on if there will not be any apps resolved       * anyway.       */ -    void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) { +    public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {          final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);          if (emptyState == null) { @@ -319,9 +425,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          if (emptyState.getButtonClickListener() != null) {              clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { -                ProfileDescriptor descriptor = getItem( +                ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(                          userHandleToPageIndex(listAdapter.getUserHandle())); -                AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView()); +                descriptor.mEmptyStateUi.showSpinner();              });          } @@ -340,45 +446,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          }      } -    /** -     * Utility class to check if there are cross profile intents, it is in a separate class so -     * it could be mocked in tests -     */ -    public static class CrossProfileIntentsChecker { - -        private final ContentResolver mContentResolver; - -        public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) { -            mContentResolver = contentResolver; -        } - -        /** -         * Returns {@code true} if at least one of the provided {@code intents} can be forwarded -         * from {@code source} (user id) to {@code target} (user id). -         */ -        public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source, -                @UserIdInt int target) { -            IPackageManager packageManager = AppGlobals.getPackageManager(); - -            return intents.stream().anyMatch(intent -> -                    null != IntentForwarderActivity.canForward(intent, source, target, -                            packageManager, mContentResolver)); -        } -    } - -    protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState, +    protected void showEmptyState( +            ListAdapterT activeListAdapter, +            EmptyState emptyState,              View.OnClickListener buttonOnClick) { -        ProfileDescriptor descriptor = getItem( +        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(                  userHandleToPageIndex(activeListAdapter.getUserHandle())); -        descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE); +        descriptor.mRootView.findViewById( +                com.android.internal.R.id.resolver_list).setVisibility(View.GONE); +        descriptor.mEmptyStateUi.resetViewVisibilities(); +          ViewGroup emptyStateView = descriptor.getEmptyStateView(); -        resetViewVisibilitiesForEmptyState(emptyStateView); -        emptyStateView.setVisibility(View.VISIBLE); -        View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container); +        View container = emptyStateView.findViewById( +                com.android.internal.R.id.resolver_empty_state_container);          setupContainerPadding(container); -        TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title); +        TextView titleView = emptyStateView.findViewById( +                com.android.internal.R.id.resolver_empty_state_title);          String title = emptyState.getTitle();          if (title != null) {              titleView.setVisibility(View.VISIBLE); @@ -387,7 +472,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {              titleView.setVisibility(View.GONE);          } -        TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle); +        TextView subtitleView = emptyStateView.findViewById( +                com.android.internal.R.id.resolver_empty_state_subtitle);          String subtitle = emptyState.getSubtitle();          if (subtitle != null) {              subtitleView.setVisibility(View.VISIBLE); @@ -399,7 +485,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);          defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE); -        Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button); +        Button button = emptyStateView.findViewById( +                com.android.internal.R.id.resolver_empty_state_button);          button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);          button.setOnClickListener(buttonOnClick); @@ -410,44 +497,50 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {       * Sets up the padding of the view containing the empty state screens.       * <p>This method is meant to be overridden so that subclasses can customize the padding.       */ -    protected void setupContainerPadding(View container) {} - -    private void showSpinner(View emptyStateView) { -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); -    } - -    private void resetViewVisibilitiesForEmptyState(View emptyStateView) { -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); -        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE); -        emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); -    } - -    protected void showListView(ResolverListAdapter activeListAdapter) { -        ProfileDescriptor descriptor = getItem( +    public void setupContainerPadding(View container) { +        Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); +        bottomPaddingOverride.ifPresent(paddingBottom -> +                container.setPadding( +                    container.getPaddingLeft(), +                    container.getPaddingTop(), +                    container.getPaddingRight(), +                    paddingBottom)); +    } + +    public void showListView(ListAdapterT activeListAdapter) { +        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(                  userHandleToPageIndex(activeListAdapter.getUserHandle())); -        descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE); -        View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state); -        emptyStateView.setVisibility(View.GONE); +        descriptor.mRootView.findViewById( +                com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE); +        descriptor.mEmptyStateUi.hide();      } -    boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) { +    public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {          int count = listAdapter.getUnfilteredCount();          return (count == 0 && listAdapter.getPlaceholderCount() == 0)                  || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)                      && mWorkProfileQuietModeChecker.get());      } -    protected static class ProfileDescriptor { -        final ViewGroup rootView; +    // 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; -        ProfileDescriptor(ViewGroup rootView) { -            this.rootView = rootView; + +        private final SinglePageAdapterT mAdapter; +        private final PageViewT mView; + +        ProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) { +            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);          }          protected ViewGroup getEmptyStateView() { @@ -455,6 +548,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {          }      } +    /** 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. @@ -478,102 +572,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {      }      /** -     * Returns an empty state to show for the current profile page (tab) if necessary. -     * This could be used e.g. to show a blocker on a tab if device management policy doesn't -     * allow to use it or there are no apps available. -     */ -    public interface EmptyStateProvider { -        /** -         * When a non-null empty state is returned the corresponding profile page will show -         * this empty state -         * @param resolverListAdapter the current adapter -         */ -        @Nullable -        default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { -            return null; -        } -    } - -    /** -     * Empty state provider that combines multiple providers. Providers earlier in the list have -     * priority, that is if there is a provider that returns non-null empty state then all further -     * providers will be ignored. -     */ -    public static class CompositeEmptyStateProvider implements EmptyStateProvider { - -        private final EmptyStateProvider[] mProviders; - -        public CompositeEmptyStateProvider(EmptyStateProvider... providers) { -            mProviders = providers; -        } - -        @Nullable -        @Override -        public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { -            for (EmptyStateProvider provider : mProviders) { -                EmptyState emptyState = provider.getEmptyState(resolverListAdapter); -                if (emptyState != null) { -                    return emptyState; -                } -            } -            return null; -        } -    } - -    /** -     * Describes how the blocked empty state should look like for a profile tab -     */ -    public interface EmptyState { -        /** -         * Title that will be shown on the empty state -         */ -        @Nullable -        default String getTitle() { return null; } - -        /** -         * Subtitle that will be shown underneath the title on the empty state -         */ -        @Nullable -        default String getSubtitle()  { return null; } - -        /** -         * If non-null then a button will be shown and this listener will be called -         * when the button is clicked -         */ -        @Nullable -        default ClickListener getButtonClickListener()  { return null; } - -        /** -         * If true then default text ('No apps can perform this action') and style for the empty -         * state will be applied, title and subtitle will be ignored. -         */ -        default boolean useDefaultEmptyView() { return false; } - -        /** -         * Returns true if for this empty state we should skip rebuilding of the apps list -         * for this tab. -         */ -        default boolean shouldSkipDataRebuild() { return false; } - -        /** -         * Called when empty state is shown, could be used e.g. to track analytics events -         */ -        default void onEmptyStateShown() {} - -        interface ClickListener { -            void onClick(TabControl currentTab); -        } - -        interface TabControl { -            void showSpinner(); -        } -    } - - -    /**       * Listener for when the user switches on the work profile from the work tab.       */ -    interface OnSwitchOnWorkSelectedListener { +    public interface OnSwitchOnWorkSelectedListener {          /**           * Callback for when the user switches on the work profile from the work tab.           */ diff --git a/java/src/com/android/intentresolver/ResolvedComponentInfo.java b/java/src/com/android/intentresolver/ResolvedComponentInfo.java index ecb72cbf..aaa97c42 100644 --- a/java/src/com/android/intentresolver/ResolvedComponentInfo.java +++ b/java/src/com/android/intentresolver/ResolvedComponentInfo.java @@ -20,6 +20,8 @@ import android.content.ComponentName;  import android.content.Intent;  import android.content.pm.ResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; +  import java.util.ArrayList;  import java.util.List; @@ -86,7 +88,7 @@ public final class ResolvedComponentInfo {      }      /** -     * @return whether this component was pinned by a call to {@link #setPinned()}. +     * @return whether this component was pinned by a call to {@link #setPinned}.       * TODO: consolidate sources of pinning data and/or document how this differs from other places       * we make a "pinning" determination.       */ diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 35c7e897..0331c33e 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -36,9 +36,6 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE  import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; -import android.annotation.Nullable; -import android.annotation.StringRes; -import android.annotation.UiThread;  import android.app.Activity;  import android.app.ActivityManager;  import android.app.ActivityThread; @@ -96,18 +93,26 @@ 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.viewpager.widget.ViewPager; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; -import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.MultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.MultiProfilePagerAdapter.Profile;  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo; +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.emptystate.NoAppsAvailableEmptyStateProvider; +import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; +import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider;  import com.android.intentresolver.icons.DefaultTargetDataLoader;  import com.android.intentresolver.icons.TargetDataLoader;  import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; @@ -199,8 +204,10 @@ public class ResolverActivity extends FragmentActivity implements      private PackageMonitor mPersonalPackageMonitor;      private PackageMonitor mWorkPackageMonitor; +    private TargetDataLoader mTargetDataLoader; +      @VisibleForTesting -    protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter; +    protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;      protected WorkProfileAvailabilityManager mWorkProfileAvailability; @@ -227,8 +234,8 @@ public class ResolverActivity extends FragmentActivity implements      static final String EXTRA_CALLING_USER =              "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; -    protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; -    protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; +    protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; +    protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;      private UserHandle mHeaderCreatorUser; @@ -239,11 +246,20 @@ public class ResolverActivity extends FragmentActivity implements      // new component whose lifecycle is limited to the "created" Activity (so that we can just hold      // the annotations as a `final` ivar, which is a better way to show immutability).      private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> { -        final AnnotatedUserHandles result = AnnotatedUserHandles.forShareActivity(this); +        final AnnotatedUserHandles result = computeAnnotatedUserHandles();          mLazyAnnotatedUserHandles = () -> result;          return result;      }; +    // This method is called exactly once during creation to compute the immutable annotations +    // accessible through the lazy supplier {@link mLazyAnnotatedUserHandles}. +    // TODO: this is only defined so that tests can provide an override that injects fake +    // annotations. Dagger could provide a cleaner model for our testing/injection requirements. +    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) +    protected AnnotatedUserHandles computeAnnotatedUserHandles() { +        return AnnotatedUserHandles.forShareActivity(this); +    } +      @Nullable      private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; @@ -418,6 +434,7 @@ public class ResolverActivity extends FragmentActivity implements          mSupportsAlwaysUseOption = supportsAlwaysUseOption;          mSafeForwardingMode = safeForwardingMode; +        mTargetDataLoader = targetDataLoader;          // 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 @@ -438,11 +455,12 @@ public class ResolverActivity extends FragmentActivity implements          mPersonalPackageMonitor = createPackageMonitor(                  mMultiProfilePagerAdapter.getPersonalListAdapter());          mPersonalPackageMonitor.register( -                this, getMainLooper(), getPersonalProfileUserHandle(), false); +                this, getMainLooper(), getAnnotatedUserHandles().personalProfileUserHandle, false);          if (shouldShowTabs()) {              mWorkPackageMonitor = createPackageMonitor(                      mMultiProfilePagerAdapter.getWorkListAdapter()); -            mWorkPackageMonitor.register(this, getMainLooper(), getWorkProfileUserHandle(), false); +            mWorkPackageMonitor.register( +                    this, getMainLooper(), getAnnotatedUserHandles().workProfileUserHandle, false);          }          mRegistered = true; @@ -484,12 +502,12 @@ public class ResolverActivity extends FragmentActivity implements                          + (categories != null ? Arrays.toString(categories.toArray()) : ""));      } -    protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( +    protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(              Intent[] initialIntents,              List<ResolveInfo> resolutionList,              boolean filterLastUsed,              TargetDataLoader targetDataLoader) { -        AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; +        MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;          if (shouldShowTabs()) {              resolverMultiProfilePagerAdapter =                      createResolverMultiProfilePagerAdapterForTwoProfiles( @@ -509,9 +527,9 @@ public class ResolverActivity extends FragmentActivity implements              return new EmptyStateProvider() {};          } -        final AbstractMultiProfilePagerAdapter.EmptyState -                noWorkToPersonalEmptyState = -                new DevicePolicyBlockerEmptyState(/* context= */ this, +        final EmptyState noWorkToPersonalEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this,                          /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,                          /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,                          /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL, @@ -521,8 +539,9 @@ public class ResolverActivity extends FragmentActivity implements                          /* devicePolicyEventCategory= */                                  ResolverActivity.METRICS_CATEGORY_RESOLVER); -        final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState = -                new DevicePolicyBlockerEmptyState(/* context= */ this, +        final EmptyState noPersonalToWorkEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this,                          /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,                          /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,                          /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK, @@ -532,9 +551,12 @@ public class ResolverActivity extends FragmentActivity implements                          /* devicePolicyEventCategory= */                                  ResolverActivity.METRICS_CATEGORY_RESOLVER); -        return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), -                noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, -                createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); +        return new NoCrossProfileEmptyStateProvider( +                getAnnotatedUserHandles().personalProfileUserHandle, +                noWorkToPersonalEmptyState, +                noPersonalToWorkEmptyState, +                createCrossProfileIntentsChecker(), +                getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);      }      protected int appliedThemeResId() { @@ -591,7 +613,7 @@ public class ResolverActivity extends FragmentActivity implements      }      @Override -    public void onConfigurationChanged(Configuration newConfig) { +    public void onConfigurationChanged(@NonNull Configuration newConfig) {          super.onConfigurationChanged(newConfig);          mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();          if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault() @@ -1014,7 +1036,7 @@ public class ResolverActivity extends FragmentActivity implements      @Override // ResolverListCommunicator      public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {          if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) { -            if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle()) +            if (listAdapter.getUserHandle().equals(getAnnotatedUserHandles().workProfileUserHandle)                      && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {                  // We have just turned on the work profile and entered the pass code to start it,                  // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no @@ -1052,16 +1074,15 @@ public class ResolverActivity extends FragmentActivity implements      }      protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { -        final UserHandle workUser = getWorkProfileUserHandle(); -          return new WorkProfileAvailabilityManager(                  getSystemService(UserManager.class), -                workUser, +                getAnnotatedUserHandles().workProfileUserHandle,                  this::onWorkProfileStatusUpdated);      }      protected void onWorkProfileStatusUpdated() { -        if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getWorkProfileUserHandle())) { +        if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals( +                getAnnotatedUserHandles().workProfileUserHandle)) {              mMultiProfilePagerAdapter.rebuildActiveTab(true);          } else {              mMultiProfilePagerAdapter.clearInactiveProfileCache(); @@ -1079,8 +1100,8 @@ public class ResolverActivity extends FragmentActivity implements              UserHandle userHandle,              TargetDataLoader targetDataLoader) {          UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() -                && userHandle.equals(getPersonalProfileUserHandle()) -                ? getCloneProfileUserHandle() : userHandle; +                && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle) +                ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;          return new ResolverListAdapter(                  context,                  payloadIntents, @@ -1136,9 +1157,9 @@ public class ResolverActivity extends FragmentActivity implements          final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(                  this,                  workProfileUserHandle, -                getPersonalProfileUserHandle(), +                getAnnotatedUserHandles().personalProfileUserHandle,                  getMetricsCategory(), -                getTabOwnerUserHandleForLaunch() +                getAnnotatedUserHandles().tabOwnerUserHandleForLaunch          );          // Return composite provider, the order matters (the higher, the more priority) @@ -1188,7 +1209,7 @@ public class ResolverActivity extends FragmentActivity implements                  initialIntents,                  resolutionList,                  filterLastUsed, -                /* userHandle */ getPersonalProfileUserHandle(), +                /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,                  targetDataLoader);          return new ResolverMultiProfilePagerAdapter(                  /* context */ this, @@ -1196,13 +1217,13 @@ public class ResolverActivity extends FragmentActivity implements                  createEmptyStateProvider(/* workProfileUserHandle= */ null),                  /* workProfileQuietModeChecker= */ () -> false,                  /* workProfileUserHandle= */ null, -                getCloneProfileUserHandle()); +                getAnnotatedUserHandles().cloneProfileUserHandle);      }      private UserHandle getIntentUser() {          return getIntent().hasExtra(EXTRA_CALLING_USER)                  ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) -                : getTabOwnerUserHandleForLaunch(); +                : getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;      }      private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( @@ -1215,10 +1236,10 @@ public class ResolverActivity extends FragmentActivity implements          // this happens, we check for it here and set the current profile's tab.          int selectedProfile = getCurrentProfile();          UserHandle intentUser = getIntentUser(); -        if (!getTabOwnerUserHandleForLaunch().equals(intentUser)) { -            if (getPersonalProfileUserHandle().equals(intentUser)) { +        if (!getAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) { +            if (getAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {                  selectedProfile = PROFILE_PERSONAL; -            } else if (getWorkProfileUserHandle().equals(intentUser)) { +            } else if (getAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {                  selectedProfile = PROFILE_WORK;              }          } else { @@ -1236,10 +1257,10 @@ public class ResolverActivity extends FragmentActivity implements                  selectedProfile == PROFILE_PERSONAL ? initialIntents : null,                  resolutionList,                  (filterLastUsed && UserHandle.myUserId() -                        == getPersonalProfileUserHandle().getIdentifier()), -                /* userHandle */ getPersonalProfileUserHandle(), +                        == getAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()), +                /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,                  targetDataLoader); -        UserHandle workProfileUserHandle = getWorkProfileUserHandle(); +        UserHandle workProfileUserHandle = getAnnotatedUserHandles().workProfileUserHandle;          ResolverListAdapter workAdapter = createResolverListAdapter(                  /* context */ this,                  /* payloadIntents */ mIntents, @@ -1253,11 +1274,11 @@ public class ResolverActivity extends FragmentActivity implements                  /* context */ this,                  personalAdapter,                  workAdapter, -                createEmptyStateProvider(getWorkProfileUserHandle()), +                createEmptyStateProvider(workProfileUserHandle),                  () -> mWorkProfileAvailability.isQuietModeEnabled(),                  selectedProfile, -                getWorkProfileUserHandle(), -                getCloneProfileUserHandle()); +                workProfileUserHandle, +                getAnnotatedUserHandles().cloneProfileUserHandle);      }      /** @@ -1280,55 +1301,29 @@ public class ResolverActivity extends FragmentActivity implements      }      protected final @Profile int getCurrentProfile() { -        return (getTabOwnerUserHandleForLaunch().equals(getPersonalProfileUserHandle()) -                ? PROFILE_PERSONAL : PROFILE_WORK); +        UserHandle launchUser = getAnnotatedUserHandles().tabOwnerUserHandleForLaunch; +        UserHandle personalUser = getAnnotatedUserHandles().personalProfileUserHandle; +        return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;      }      protected final AnnotatedUserHandles getAnnotatedUserHandles() {          return mLazyAnnotatedUserHandles.get();      } -    protected final UserHandle getPersonalProfileUserHandle() { -        return getAnnotatedUserHandles().personalProfileUserHandle; -    } - -    // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. -    // @NonFinalForTesting -    @Nullable -    protected UserHandle getWorkProfileUserHandle() { -        return getAnnotatedUserHandles().workProfileUserHandle; -    } - -    // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. -    @Nullable -    protected UserHandle getCloneProfileUserHandle() { -        return getAnnotatedUserHandles().cloneProfileUserHandle; -    } - -    // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. -    protected UserHandle getTabOwnerUserHandleForLaunch() { -        return getAnnotatedUserHandles().tabOwnerUserHandleForLaunch; -    } - -    protected UserHandle getUserHandleSharesheetLaunchedAs() { -        return getAnnotatedUserHandles().userHandleSharesheetLaunchedAs; -    } - -      private boolean hasWorkProfile() { -        return getWorkProfileUserHandle() != null; +        return getAnnotatedUserHandles().workProfileUserHandle != null;      }      private boolean hasCloneProfile() { -        return getCloneProfileUserHandle() != null; +        return getAnnotatedUserHandles().cloneProfileUserHandle != null;      }      protected final boolean isLaunchedAsCloneProfile() { -        return hasCloneProfile() -                && getUserHandleSharesheetLaunchedAs().equals(getCloneProfileUserHandle()); +        UserHandle launchUser = getAnnotatedUserHandles().userHandleSharesheetLaunchedAs; +        UserHandle cloneUser = getAnnotatedUserHandles().cloneProfileUserHandle; +        return hasCloneProfile() && launchUser.equals(cloneUser);      } -      protected final boolean shouldShowTabs() {          return hasWorkProfile();      } @@ -1368,7 +1363,9 @@ public class ResolverActivity extends FragmentActivity implements          }          DevicePolicyEventLogger                  .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED) -                .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle())) +                .setBoolean( +                        currentUserHandle.equals( +                                getAnnotatedUserHandles().personalProfileUserHandle))                  .setStrings(getMetricsCategory(),                          cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")                  .write(); @@ -1399,7 +1396,7 @@ public class ResolverActivity extends FragmentActivity implements      }      final Option optionForChooserTarget(TargetInfo target, int index) { -        return new Option(target.getDisplayLabel(), index); +        return new Option(getOrLoadDisplayLabel(target), index);      }      public final Intent getTargetIntent() { @@ -1475,8 +1472,11 @@ public class ResolverActivity extends FragmentActivity implements              return getString(defaultTitleRes);          } else {              return named -                    ? getString(title.namedTitleRes, mMultiProfilePagerAdapter -                            .getActiveListAdapter().getFilteredItem().getDisplayLabel()) +                    ? getString( +                            title.namedTitleRes, +                            getOrLoadDisplayLabel( +                                    mMultiProfilePagerAdapter +                                        .getActiveListAdapter().getFilteredItem()))                      : getString(title.titleRes);          }      } @@ -1491,15 +1491,21 @@ public class ResolverActivity extends FragmentActivity implements      protected final void onRestart() {          super.onRestart();          if (!mRegistered) { -            mPersonalPackageMonitor.register(this, getMainLooper(), -                    getPersonalProfileUserHandle(), false); +            mPersonalPackageMonitor.register( +                    this, +                    getMainLooper(), +                    getAnnotatedUserHandles().personalProfileUserHandle, +                    false);              if (shouldShowTabs()) {                  if (mWorkPackageMonitor == null) {                      mWorkPackageMonitor = createPackageMonitor(                              mMultiProfilePagerAdapter.getWorkListAdapter());                  } -                mWorkPackageMonitor.register(this, getMainLooper(), -                        getWorkProfileUserHandle(), false); +                mWorkPackageMonitor.register( +                        this, +                        getMainLooper(), +                        getAnnotatedUserHandles().workProfileUserHandle, +                        false);              }              mRegistered = true;          } @@ -1523,7 +1529,7 @@ public class ResolverActivity extends FragmentActivity implements      }      @Override -    protected final void onSaveInstanceState(Bundle outState) { +    protected final void onSaveInstanceState(@NonNull Bundle outState) {          super.onSaveInstanceState(outState);          ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);          if (viewPager != null) { @@ -1532,7 +1538,7 @@ public class ResolverActivity extends FragmentActivity implements      }      @Override -    protected final void onRestoreInstanceState(Bundle savedInstanceState) { +    protected final void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {          super.onRestoreInstanceState(savedInstanceState);          resetButtonBar();          ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); @@ -1807,9 +1813,10 @@ public class ResolverActivity extends FragmentActivity implements          ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(                  getResources().getString( -                        inWorkProfile ? R.string.miniresolver_open_in_personal +                        inWorkProfile +                                ? R.string.miniresolver_open_in_personal                                  : R.string.miniresolver_open_in_work, -                        otherProfileResolveInfo.getDisplayLabel())); +                        getOrLoadDisplayLabel(otherProfileResolveInfo)));          ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(                  inWorkProfile ? R.string.miniresolver_use_work_browser                          : R.string.miniresolver_use_personal_browser); @@ -1973,7 +1980,7 @@ public class ResolverActivity extends FragmentActivity implements          DevicePolicyEventLogger                  .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)                  .setBoolean(activeListAdapter.getUserHandle() -                        .equals(getPersonalProfileUserHandle())) +                        .equals(getAnnotatedUserHandles().personalProfileUserHandle))                  .setStrings(getMetricsCategory())                  .write();          safelyStartActivity(activeProfileTarget); @@ -2080,7 +2087,7 @@ public class ResolverActivity extends FragmentActivity implements          viewPager.setVisibility(View.VISIBLE);          tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());          mMultiProfilePagerAdapter.setOnProfileSelectedListener( -                new AbstractMultiProfilePagerAdapter.OnProfileSelectedListener() { +                new MultiProfilePagerAdapter.OnProfileSelectedListener() {                      @Override                      public void onProfileSelected(int index) {                          tabHost.setCurrentTab(index); @@ -2256,7 +2263,7 @@ public class ResolverActivity extends FragmentActivity implements          // filtered item. We always show the same default app even in the inactive user profile.          boolean adapterForCurrentUserHasFilteredItem =                  mMultiProfilePagerAdapter.getListAdapterForUserHandle( -                        getTabOwnerUserHandleForLaunch()).hasFilteredItem(); +                        getAnnotatedUserHandles().tabOwnerUserHandleForLaunch).hasFilteredItem();          return mSupportsAlwaysUseOption && adapterForCurrentUserHasFilteredItem;      } @@ -2268,20 +2275,6 @@ public class ResolverActivity extends FragmentActivity implements          mRetainInOnStop = retainInOnStop;      } -    /** -     * Check a simple match for the component of two ResolveInfos. -     */ -    @Override // ResolverListCommunicator -    public final boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) { -        return lhs == null ? rhs == null -                : lhs.activityInfo == null ? rhs.activityInfo == null -                : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name) -                && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName) -                        // Comparing against resolveInfo.userHandle in case cloned apps are present, -                        // as they will have the same activityInfo. -                && Objects.equals(lhs.userHandle, rhs.userHandle); -    } -      private boolean inactiveListAdapterHasItems() {          if (!shouldShowTabs()) {              return false; @@ -2391,7 +2384,7 @@ public class ResolverActivity extends FragmentActivity implements       * {@link ResolverListController} configured for the provided {@code userHandle}.       */      protected final UserHandle getQueryIntentsUser(UserHandle userHandle) { -        return mLazyAnnotatedUserHandles.get().getQueryIntentsUser(userHandle); +        return getAnnotatedUserHandles().getQueryIntentsUser(userHandle);      }      /** @@ -2411,10 +2404,18 @@ public class ResolverActivity extends FragmentActivity implements          // Add clonedProfileUserHandle to the list only if we are:          // a. Building the Personal Tab.          // b. CloneProfile exists on the device. -        if (userHandle.equals(getPersonalProfileUserHandle()) -                && getCloneProfileUserHandle() != null) { -            userList.add(getCloneProfileUserHandle()); +        if (userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle) +                && hasCloneProfile()) { +            userList.add(getAnnotatedUserHandles().cloneProfileUserHandle);          }          return userList;      } + +    private CharSequence getOrLoadDisplayLabel(TargetInfo info) { +        if (info.isDisplayResolveInfo()) { +            mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info); +        } +        CharSequence displayLabel = info.getDisplayLabel(); +        return displayLabel == null ? "" : displayLabel; +    }  } diff --git a/java/src/com/android/intentresolver/ResolverInfoHelpers.kt b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt new file mode 100644 index 00000000..8d1d8658 --- /dev/null +++ b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@file:JvmName("ResolveInfoHelpers") + +package com.android.intentresolver + +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo + +fun resolveInfoMatch(lhs: ResolveInfo?, rhs: ResolveInfo?): Boolean = +    (lhs === rhs) || +        ((lhs != null && rhs != null) && +            activityInfoMatch(lhs.activityInfo, rhs.activityInfo) && +            // Comparing against resolveInfo.userHandle in case cloned apps are present, +            // as they will have the same activityInfo. +            lhs.userHandle == rhs.userHandle) + +private fun activityInfoMatch(lhs: ActivityInfo?, rhs: ActivityInfo?): Boolean = +    (lhs === rhs) || +        (lhs != null && rhs != null && lhs.name == rhs.name && lhs.packageName == rhs.packageName) diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 282a672f..564d8d19 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -16,8 +16,6 @@  package com.android.intentresolver; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.content.Context;  import android.content.Intent;  import android.content.pm.ActivityInfo; @@ -27,6 +25,7 @@ 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; @@ -42,8 +41,14 @@ import android.widget.BaseAdapter;  import android.widget.ImageView;  import android.widget.TextView; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.LabelInfo;  import com.android.intentresolver.icons.TargetDataLoader;  import com.android.internal.annotations.VisibleForTesting; @@ -53,6 +58,8 @@ import java.util.ArrayList;  import java.util.HashSet;  import java.util.List;  import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean;  public class ResolverListAdapter extends BaseAdapter {      private static final String TAG = "ResolverListAdapter"; @@ -63,7 +70,7 @@ public class ResolverListAdapter extends BaseAdapter {      protected final Context mContext;      protected final LayoutInflater mInflater;      protected final ResolverListCommunicator mResolverListCommunicator; -    protected final ResolverListController mResolverListController; +    public final ResolverListController mResolverListController;      private final List<Intent> mIntents;      private final Intent[] mInitialIntents; @@ -75,6 +82,9 @@ public class ResolverListAdapter extends BaseAdapter {      private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>();      private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>(); +    private final Executor mBgExecutor; +    private final Executor mCallbackExecutor; +    private final AtomicBoolean mDestroyed = new AtomicBoolean();      private ResolveInfo mLastChosen;      private DisplayResolveInfo mOtherProfile; @@ -86,7 +96,6 @@ public class ResolverListAdapter extends BaseAdapter {      private int mLastChosenPosition = -1;      private final boolean mFilterLastUsed; -    private Runnable mPostListReadyRunnable;      private boolean mIsTabLoaded;      // Represents the UserSpace in which the Initial Intents should be resolved.      private final UserHandle mInitialIntentsUserSpace; @@ -103,6 +112,37 @@ public class ResolverListAdapter extends BaseAdapter {              ResolverListCommunicator resolverListCommunicator,              UserHandle initialIntentsUserSpace,              TargetDataLoader targetDataLoader) { +        this( +                context, +                payloadIntents, +                initialIntents, +                rList, +                filterLastUsed, +                resolverListController, +                userHandle, +                targetIntent, +                resolverListCommunicator, +                initialIntentsUserSpace, +                targetDataLoader, +                AsyncTask.SERIAL_EXECUTOR, +                runnable -> context.getMainThreadHandler().post(runnable)); +    } + +    @VisibleForTesting +    public ResolverListAdapter( +            Context context, +            List<Intent> payloadIntents, +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            ResolverListController resolverListController, +            UserHandle userHandle, +            Intent targetIntent, +            ResolverListCommunicator resolverListCommunicator, +            UserHandle initialIntentsUserSpace, +            TargetDataLoader targetDataLoader, +            Executor bgExecutor, +            Executor callbackExecutor) {          mContext = context;          mIntents = payloadIntents;          mInitialIntents = initialIntents; @@ -117,6 +157,12 @@ public class ResolverListAdapter extends BaseAdapter {          mTargetIntent = targetIntent;          mResolverListCommunicator = resolverListCommunicator;          mInitialIntentsUserSpace = initialIntentsUserSpace; +        mBgExecutor = bgExecutor; +        mCallbackExecutor = callbackExecutor; +    } + +    protected Intent getTargetIntent() { +        return mTargetIntent;      }      public final DisplayResolveInfo getFirstDisplayResolveInfo() { @@ -189,18 +235,18 @@ public class ResolverListAdapter extends BaseAdapter {                  packageName, userHandle, action);      } -    List<ResolvedComponentInfo> getUnfilteredResolveList() { +    public List<ResolvedComponentInfo> getUnfilteredResolveList() {          return mUnfilteredResolveList;      }      /**       * Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady} -     * callback on the main handler with {@code rebuildCompleted} true. +     * callback on the callback executor with {@code rebuildCompleted} true.       *       * In some cases some parts will need some asynchronous work to complete. Then this will first -     * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted} -     * false; only when the asynchronous work completes will this then go on to queue another -     * {@code onPostListReady} callback with {@code rebuildCompleted} true. +     * immediately queue {@code onPostListReady} (on the callback executor) with +     * {@code rebuildCompleted} false; only when the asynchronous work completes will this then go +     * on to queue another {@code onPostListReady} callback with {@code rebuildCompleted} true.       *       * The {@code doPostProcessing} parameter is used to specify whether to update the UI and       * load additional targets (e.g. direct share) after the list has been rebuilt. We may choose @@ -212,7 +258,7 @@ public class ResolverListAdapter extends BaseAdapter {       * with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work.       * Otherwise the callback is only queued once, with {@code rebuildCompleted} true.       */ -    protected boolean rebuildList(boolean doPostProcessing) { +    public boolean rebuildList(boolean doPostProcessing) {          Trace.beginSection("ResolverListAdapter#rebuildList");          mDisplayList.clear();          mIsTabLoaded = false; @@ -357,8 +403,8 @@ public class ResolverListAdapter extends BaseAdapter {                      otherProfileInfo,                      mPm,                      mTargetIntent, -                    mResolverListCommunicator, -                    mTargetDataLoader); +                    mResolverListCommunicator +            );          } else {              mOtherProfile = null;              try { @@ -402,35 +448,42 @@ public class ResolverListAdapter extends BaseAdapter {          // Send an "incomplete" list-ready while the async task is running.          postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false); -        createSortingTask(doPostProcessing).execute(filteredResolveList); +        mBgExecutor.execute(() -> { +            List<ResolvedComponentInfo> sortedComponents = null; +            //TODO: the try-catch logic here is to formally match the AsyncTask's behavior. +            // Empirically, we don't need it as in the case on an exception, the app will crash and +            // `onComponentsSorted` won't be invoked. +            try { +                sortComponents(filteredResolveList); +                sortedComponents = filteredResolveList; +            } catch (Throwable t) { +                Log.e(TAG, "Failed to sort components", t); +                throw t; +            } finally { +                final List<ResolvedComponentInfo> result = sortedComponents; +                mCallbackExecutor.execute(() -> onComponentsSorted(result, doPostProcessing)); +            } +        });          return false;      } -    AsyncTask<List<ResolvedComponentInfo>, -            Void, -            List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { -        return new AsyncTask<List<ResolvedComponentInfo>, -                Void, -                List<ResolvedComponentInfo>>() { -            @Override -            protected List<ResolvedComponentInfo> doInBackground( -                    List<ResolvedComponentInfo>... params) { -                mResolverListController.sort(params[0]); -                return params[0]; -            } -            @Override -            protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { -                processSortedList(sortedComponents, doPostProcessing); -                notifyDataSetChanged(); -                if (doPostProcessing) { -                    mResolverListCommunicator.updateProfileViewButton(); -                } -            } -        }; +    @WorkerThread +    protected void sortComponents(List<ResolvedComponentInfo> components) { +        mResolverListController.sort(components);      } -    protected void processSortedList(List<ResolvedComponentInfo> sortedComponents, -            boolean doPostProcessing) { +    @MainThread +    protected void onComponentsSorted( +            @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) { +        processSortedList(sortedComponents, doPostProcessing); +        notifyDataSetChanged(); +        if (doPostProcessing) { +            mResolverListCommunicator.updateProfileViewButton(); +        } +    } + +    protected void processSortedList( +            @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {          final int n = sortedComponents != null ? sortedComponents.size() : 0;          Trace.beginSection("ResolverListAdapter#processSortedList:" + n);          if (n != 0) { @@ -471,8 +524,7 @@ public class ResolverListAdapter extends BaseAdapter {                              ri,                              ri.loadLabel(mPm),                              null, -                            ii, -                            mTargetDataLoader.createPresentationGetter(ri))); +                            ii));                  }              } @@ -494,23 +546,23 @@ public class ResolverListAdapter extends BaseAdapter {      /**       * Some necessary methods for creating the list are initiated in onCreate and will also       * determine the layout known. We therefore can't update the UI inline and post to the -     * handler thread to update after the current task is finished. +     * callback executor to update after the current task is finished.       * @param doPostProcessing Whether to update the UI and load additional direct share targets       *                         after the list has been rebuilt       * @param rebuildCompleted Whether the list has been completely rebuilt       */ -    void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) { -        if (mPostListReadyRunnable == null) { -            mPostListReadyRunnable = new Runnable() { -                @Override -                public void run() { -                    mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, -                            doPostProcessing, rebuildCompleted); -                    mPostListReadyRunnable = null; +    public void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) { +        Runnable listReadyRunnable = new Runnable() { +            @Override +            public void run() { +                if (mDestroyed.get()) { +                    return;                  } -            }; -            mContext.getMainThreadHandler().post(mPostListReadyRunnable); -        } +                mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, +                        doPostProcessing, rebuildCompleted); +            } +        }; +        mCallbackExecutor.execute(listReadyRunnable);      }      private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { @@ -524,8 +576,7 @@ public class ResolverListAdapter extends BaseAdapter {          final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(                  intent,                  add, -                (replaceIntent != null) ? replaceIntent : defaultIntent, -                mTargetDataLoader.createPresentationGetter(add)); +                (replaceIntent != null) ? replaceIntent : defaultIntent);          dri.setPinned(rci.isPinned());          if (rci.isPinned()) {              Log.i(TAG, "Pinned item: " + rci.name); @@ -572,7 +623,7 @@ public class ResolverListAdapter extends BaseAdapter {      protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {          // Checks if this info is already listed in display.          for (DisplayResolveInfo existingInfo : mDisplayList) { -            if (mResolverListCommunicator +            if (ResolveInfoHelpers                      .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {                  return false;              } @@ -710,27 +761,25 @@ public class ResolverListAdapter extends BaseAdapter {          }      } -    private void loadLabel(DisplayResolveInfo info) { +    protected final void loadLabel(DisplayResolveInfo info) {          if (mRequestedLabels.add(info)) {              mTargetDataLoader.loadLabel(info, (result) -> onLabelLoaded(info, result));          }      }      protected final void onLabelLoaded( -            DisplayResolveInfo displayResolveInfo, CharSequence[] result) { +            DisplayResolveInfo displayResolveInfo, LabelInfo result) {          if (displayResolveInfo.hasDisplayLabel()) {              return;          } -        displayResolveInfo.setDisplayLabel(result[0]); -        displayResolveInfo.setExtendedInfo(result[1]); +        displayResolveInfo.setDisplayLabel(result.getLabel()); +        displayResolveInfo.setExtendedInfo(result.getSubLabel());          notifyDataSetChanged();      }      public void onDestroy() { -        if (mPostListReadyRunnable != null) { -            mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); -            mPostListReadyRunnable = null; -        } +        mDestroyed.set(true); +          if (mResolverListController != null) {              mResolverListController.destroy();          } @@ -765,7 +814,7 @@ public class ResolverListAdapter extends BaseAdapter {          return mContext.getDrawable(R.drawable.resolver_icon_placeholder);      } -    void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { +    public void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {          final DisplayResolveInfo iconInfo = getFilteredItem();          if (iconInfo != null) {              mTargetDataLoader.loadAppTargetIcon( @@ -777,7 +826,7 @@ public class ResolverListAdapter extends BaseAdapter {          return mUserHandle;      } -    protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) { +    public final List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {          return mResolverListController.getResolversForIntentAsUser(                  /* shouldGetResolvedFilter= */ true,                  mResolverListCommunicator.shouldGetActivityMetadata(), @@ -786,15 +835,16 @@ public class ResolverListAdapter extends BaseAdapter {                  userHandle);      } -    protected List<Intent> getIntents() { +    public final List<Intent> getIntents() { +        // TODO: immutable copy?          return mIntents;      } -    protected boolean isTabLoaded() { +    public boolean isTabLoaded() {          return mIsTabLoaded;      } -    protected void markTabLoaded() { +    public void markTabLoaded() {          mIsTabLoaded = true;      } @@ -828,8 +878,7 @@ public class ResolverListAdapter extends BaseAdapter {              ResolvedComponentInfo resolvedComponentInfo,              PackageManager pm,              Intent targetIntent, -            ResolverListCommunicator resolverListCommunicator, -            TargetDataLoader targetDataLoader) { +            ResolverListCommunicator resolverListCommunicator) {          ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);          Intent pOrigIntent = resolverListCommunicator.getReplacementIntent( @@ -838,25 +887,19 @@ public class ResolverListAdapter extends BaseAdapter {          Intent replacementIntent = resolverListCommunicator.getReplacementIntent(                  resolveInfo.activityInfo, targetIntent); -        TargetPresentationGetter presentationGetter = -                targetDataLoader.createPresentationGetter(resolveInfo); -          return DisplayResolveInfo.newDisplayResolveInfo(                  resolvedComponentInfo.getIntentAt(0),                  resolveInfo,                  resolveInfo.loadLabel(pm),                  resolveInfo.loadLabel(pm), -                pOrigIntent != null ? pOrigIntent : replacementIntent, -                presentationGetter); +                pOrigIntent != null ? pOrigIntent : replacementIntent);      }      /**       * Necessary methods to communicate between {@link ResolverListAdapter}       * and {@link ResolverActivity}.       */ -    interface ResolverListCommunicator { - -        boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs); +    public interface ResolverListCommunicator {          Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); @@ -893,6 +936,24 @@ public class ResolverListAdapter extends BaseAdapter {          public TextView text2;          public ImageView icon; +        public final void reset() { +            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(""); + +            itemView.setContentDescription(null); +            itemView.setBackground(defaultItemViewBackground); + +            icon.setImageDrawable(null); +            icon.setColorFilter(null); +            icon.clearAnimation(); +        } +          @VisibleForTesting          public ViewHolder(View view) {              itemView = view; @@ -937,5 +998,19 @@ 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/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java index d5a5fedf..e88d766d 100644 --- a/java/src/com/android/intentresolver/ResolverListController.java +++ b/java/src/com/android/intentresolver/ResolverListController.java @@ -17,7 +17,6 @@  package com.android.intentresolver; -import android.annotation.WorkerThread;  import android.app.ActivityManager;  import android.app.AppGlobals;  import android.content.ComponentName; @@ -31,6 +30,8 @@ import android.os.RemoteException;  import android.os.UserHandle;  import android.util.Log; +import androidx.annotation.WorkerThread; +  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.model.AbstractResolverComparator; @@ -254,7 +255,6 @@ public class ResolverListController {          isComputed = true;      } -    @VisibleForTesting      @WorkerThread      public void sort(List<ResolvedComponentInfo> inputList) {          try { @@ -273,7 +273,6 @@ public class ResolverListController {          }      } -    @VisibleForTesting      @WorkerThread      public void topK(List<ResolvedComponentInfo> inputList, int k) {          if (inputList == null || inputList.isEmpty() || k <= 0) { @@ -335,7 +334,7 @@ public class ResolverListController {                  && ai.name.equals(b.name.getClassName());      } -    boolean isComponentFiltered(ComponentName componentName) { +    public boolean isComponentFiltered(ComponentName componentName) {          return false;      } diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java index 85d97ad5..591c23b7 100644 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java @@ -24,6 +24,7 @@ import android.widget.ListView;  import androidx.viewpager.widget.PagerAdapter; +import com.android.intentresolver.emptystate.EmptyStateProvider;  import com.android.internal.annotations.VisibleForTesting;  import com.google.common.collect.ImmutableList; @@ -36,10 +37,10 @@ import java.util.function.Supplier;   */  @VisibleForTesting  public class ResolverMultiProfilePagerAdapter extends -        GenericMultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> { +        MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {      private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; -    ResolverMultiProfilePagerAdapter( +    public ResolverMultiProfilePagerAdapter(              Context context,              ResolverListAdapter adapter,              EmptyStateProvider emptyStateProvider, @@ -57,14 +58,14 @@ public class ResolverMultiProfilePagerAdapter extends                  new BottomPaddingOverrideSupplier());      } -    ResolverMultiProfilePagerAdapter(Context context, -            ResolverListAdapter personalAdapter, -            ResolverListAdapter workAdapter, -            EmptyStateProvider emptyStateProvider, -            Supplier<Boolean> workProfileQuietModeChecker, -            @Profile int defaultProfile, -            UserHandle workProfileUserHandle, -            UserHandle cloneProfileUserHandle) { +    public ResolverMultiProfilePagerAdapter(Context context, +                                            ResolverListAdapter personalAdapter, +                                            ResolverListAdapter workAdapter, +                                            EmptyStateProvider emptyStateProvider, +                                            Supplier<Boolean> workProfileQuietModeChecker, +                                            @Profile int defaultProfile, +                                            UserHandle workProfileUserHandle, +                                            UserHandle cloneProfileUserHandle) {          this(                  context,                  ImmutableList.of(personalAdapter, workAdapter), @@ -86,7 +87,6 @@ public class ResolverMultiProfilePagerAdapter extends              UserHandle cloneProfileUserHandle,              BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {          super( -                context,                          listAdapter -> listAdapter,                          (listView, bindAdapter) -> listView.setAdapter(bindAdapter),                  listAdapters, diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java index 0804a2b8..0496579d 100644 --- a/java/src/com/android/intentresolver/ResolverViewPager.java +++ b/java/src/com/android/intentresolver/ResolverViewPager.java @@ -69,7 +69,7 @@ public class ResolverViewPager extends ViewPager {       * Sets whether swiping sideways should happen.       * <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).       */ -    void setSwipingEnabled(boolean swipingEnabled) { +    public void setSwipingEnabled(boolean swipingEnabled) {          mSwipingEnabled = swipingEnabled;      } diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java index 645b9391..efaaf894 100644 --- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java +++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java @@ -16,7 +16,6 @@  package com.android.intentresolver; -import android.annotation.Nullable;  import android.app.prediction.AppTarget;  import android.content.Context;  import android.content.Intent; @@ -26,6 +25,8 @@ import android.content.pm.ShortcutInfo;  import android.service.chooser.ChooserTarget;  import android.util.Log; +import androidx.annotation.Nullable; +  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.SelectableTargetInfo;  import com.android.intentresolver.chooser.TargetInfo; diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java index ec5179ac..750b24ac 100644 --- a/java/src/com/android/intentresolver/SimpleIconFactory.java +++ b/java/src/com/android/intentresolver/SimpleIconFactory.java @@ -21,9 +21,6 @@ import static android.graphics.Paint.DITHER_FLAG;  import static android.graphics.Paint.FILTER_BITMAP_FLAG;  import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; -import android.annotation.AttrRes; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.ActivityManager;  import android.content.Context;  import android.content.pm.PackageManager; @@ -50,6 +47,10 @@ import android.util.AttributeSet;  import android.util.Pools.SynchronizedPool;  import android.util.TypedValue; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +  import com.android.internal.annotations.VisibleForTesting;  import org.xmlpull.v1.XmlPullParser; @@ -719,10 +720,18 @@ public class SimpleIconFactory {          }          @Override -        public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { } +        public void inflate( +                @NonNull Resources r, +                @NonNull XmlPullParser parser, +                @NonNull AttributeSet attrs) { +        }          @Override -        public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } +        public void inflate( +                @NonNull Resources r, +                @NonNull XmlPullParser parser, +                @NonNull AttributeSet attrs, Theme theme) { +        }          /**           * Sets the scale associated with this drawable diff --git a/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java index f8b36566..910c65c9 100644 --- a/java/src/com/android/intentresolver/TargetPresentationGetter.java +++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java @@ -16,7 +16,6 @@  package com.android.intentresolver; -import android.annotation.Nullable;  import android.content.Context;  import android.content.pm.ActivityInfo;  import android.content.pm.ApplicationInfo; @@ -30,6 +29,8 @@ import android.os.UserHandle;  import android.text.TextUtils;  import android.util.Log; +import androidx.annotation.Nullable; +  /**   * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application icon   * and label over any IntentFilter or Activity icon to increase user understanding, with an @@ -37,7 +38,7 @@ import android.util.Log;   * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses   * Strings to strip creative formatting.   * - * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the + * Use one of the {@link TargetPresentationGetter.Factory} methods to create an instance of the   * appropriate concrete type.   *   * TODO: once this component (and its tests) are merged, it should be possible to refactor and diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java index 8b9bfb32..074537ef 100644 --- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java @@ -16,6 +16,8 @@  package com.android.intentresolver.chooser; +import android.service.chooser.ChooserTarget; +  import java.util.ArrayList;  import java.util.Arrays; diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index 09cf319f..536f11ce 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -16,8 +16,6 @@  package com.android.intentresolver.chooser; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.Activity;  import android.content.ComponentName;  import android.content.Intent; @@ -27,10 +25,10 @@ import android.content.pm.ResolveInfo;  import android.os.Bundle;  import android.os.UserHandle; -import com.android.intentresolver.TargetPresentationGetter; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import java.util.ArrayList; -import java.util.Arrays;  import java.util.List;  /** @@ -39,12 +37,11 @@ import java.util.List;   */  public class DisplayResolveInfo implements TargetInfo {      private final ResolveInfo mResolveInfo; -    private CharSequence mDisplayLabel; -    private CharSequence mExtendedInfo; +    private volatile CharSequence mDisplayLabel; +    private volatile CharSequence mExtendedInfo;      private final Intent mResolvedIntent;      private final List<Intent> mSourceIntents = new ArrayList<>();      private final boolean mIsSuspended; -    private TargetPresentationGetter mPresentationGetter;      private boolean mPinned = false;      private final IconHolder mDisplayIconHolder = new SettableIconHolder(); @@ -52,15 +49,13 @@ public class DisplayResolveInfo implements TargetInfo {      public static DisplayResolveInfo newDisplayResolveInfo(              Intent originalIntent,              ResolveInfo resolveInfo, -            @NonNull Intent resolvedIntent, -            @Nullable TargetPresentationGetter presentationGetter) { +            @NonNull Intent resolvedIntent) {          return newDisplayResolveInfo(                  originalIntent,                  resolveInfo,                  /* displayLabel=*/ null,                  /* extendedInfo=*/ null, -                resolvedIntent, -                presentationGetter); +                resolvedIntent);      }      /** Create a new {@code DisplayResolveInfo} instance. */ @@ -69,15 +64,13 @@ public class DisplayResolveInfo implements TargetInfo {              ResolveInfo resolveInfo,              CharSequence displayLabel,              CharSequence extendedInfo, -            @NonNull Intent resolvedIntent, -            @Nullable TargetPresentationGetter presentationGetter) { +            @NonNull Intent resolvedIntent) {          return new DisplayResolveInfo(                  originalIntent,                  resolveInfo,                  displayLabel,                  extendedInfo, -                resolvedIntent, -                presentationGetter); +                resolvedIntent);      }      private DisplayResolveInfo( @@ -85,13 +78,11 @@ public class DisplayResolveInfo implements TargetInfo {              ResolveInfo resolveInfo,              CharSequence displayLabel,              CharSequence extendedInfo, -            @NonNull Intent resolvedIntent, -            @Nullable TargetPresentationGetter presentationGetter) { +            @NonNull Intent resolvedIntent) {          mSourceIntents.add(originalIntent);          mResolveInfo = resolveInfo;          mDisplayLabel = displayLabel;          mExtendedInfo = extendedInfo; -        mPresentationGetter = presentationGetter;          final ActivityInfo ai = mResolveInfo.activityInfo;          mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; @@ -101,8 +92,7 @@ public class DisplayResolveInfo implements TargetInfo {      private DisplayResolveInfo(              DisplayResolveInfo other, -            @Nullable Intent baseIntentToSend, -            TargetPresentationGetter presentationGetter) { +            @Nullable Intent baseIntentToSend) {          mSourceIntents.addAll(other.getAllSourceIntents());          mResolveInfo = other.mResolveInfo;          mIsSuspended = other.mIsSuspended; @@ -112,7 +102,6 @@ public class DisplayResolveInfo implements TargetInfo {          mResolvedIntent = createResolvedIntent(                  baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,                  mResolveInfo.activityInfo); -        mPresentationGetter = presentationGetter;          mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());      } @@ -124,7 +113,6 @@ public class DisplayResolveInfo implements TargetInfo {          mDisplayLabel = other.mDisplayLabel;          mExtendedInfo = other.mExtendedInfo;          mResolvedIntent = other.mResolvedIntent; -        mPresentationGetter = other.mPresentationGetter;          mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());      } @@ -147,10 +135,6 @@ public class DisplayResolveInfo implements TargetInfo {      }      public CharSequence getDisplayLabel() { -        if (mDisplayLabel == null && mPresentationGetter != null) { -            mDisplayLabel = mPresentationGetter.getLabel(); -            mExtendedInfo = mPresentationGetter.getSubLabel(); -        }          return mDisplayLabel;      } @@ -186,8 +170,7 @@ public class DisplayResolveInfo implements TargetInfo {          return new DisplayResolveInfo(                  this, -                TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement), -                mPresentationGetter); +                TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));      }      @Override @@ -197,7 +180,7 @@ public class DisplayResolveInfo implements TargetInfo {      @Override      public ArrayList<DisplayResolveInfo> getAllDisplayTargets() { -        return new ArrayList<>(Arrays.asList(this)); +        return new ArrayList<>(List.of(this));      }      public void addAlternateSourceIntent(Intent alt) { diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java index 10d4415a..50aaec0b 100644 --- a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java @@ -16,8 +16,6 @@  package com.android.intentresolver.chooser; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.prediction.AppTarget;  import android.content.ComponentName; @@ -27,8 +25,11 @@ import android.content.pm.ResolveInfo;  import android.content.pm.ShortcutInfo;  import android.os.Bundle;  import android.os.UserHandle; +import android.service.chooser.ChooserTarget;  import android.util.HashedStringCache; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import androidx.annotation.VisibleForTesting;  import com.google.common.collect.ImmutableList; @@ -43,7 +44,7 @@ import java.util.List;  public final class ImmutableTargetInfo implements TargetInfo {      private static final String TAG = "TargetInfo"; -    /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */ +    /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics}. */      public interface TargetHashProvider {          /** Request a hash for the specified {@code target}. */          HashedStringCache.HashResult getHashedTargetIdForMetrics( @@ -53,15 +54,15 @@ public final class ImmutableTargetInfo implements TargetInfo {      /** Delegate interface to request that the target be launched by a particular API. */      public interface TargetActivityStarter {          /** -         * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the -         * specified {@code target}. +         * Request that the delegate use the {@link Activity#startActivityAsCaller} API to launch +         * the specified {@code target}.           *           * @return true if the target was launched successfully.           */          boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId);          /** -         * Request that the delegate use the {@link Activity#startAsUser()} API to launch the +         * Request that the delegate use the {@link Activity#startActivityAsUser} API to launch the           * specified {@code target}.           *           * @return true if the target was launched successfully. @@ -145,7 +146,7 @@ public final class ImmutableTargetInfo implements TargetInfo {          /**           * Configure an {@link Intent} to be built in to the output target as the "base intent to           * send," which may be a refinement of any of our source targets. This is private because -         * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever +         * it's only used internally by {@link #tryToCloneWithAppliedRefinement}; if it's ever           * expanded, the builder should probably be responsible for enforcing the refinement check.           */          private Builder setBaseIntentToSend(Intent baseIntent) { @@ -229,8 +230,8 @@ public final class ImmutableTargetInfo implements TargetInfo {         /**          * Configure the full list of source intents we could resolve for this target. This is -        * effectively the same as calling {@link #setResolvedIntent()} with the first element of -        * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those +        * effectively the same as calling {@link #setResolvedIntent} with the first element of +        * the list, and {@link #setAlternateSourceIntents} with the remainder (or clearing those          * fields on the builder if there are no corresponding elements in the list).          */          public Builder setAllSourceIntents(List<Intent> sourceIntents) { diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java index 6444e13b..46803a04 100644 --- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java @@ -16,7 +16,6 @@  package com.android.intentresolver.chooser; -import android.annotation.Nullable;  import android.app.Activity;  import android.content.Context;  import android.graphics.drawable.AnimatedVectorDrawable; @@ -24,6 +23,8 @@ import android.graphics.drawable.Drawable;  import android.os.Bundle;  import android.os.UserHandle; +import androidx.annotation.Nullable; +  import com.android.intentresolver.R;  import java.util.function.Supplier; diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java index 5766db0e..c4aa9021 100644 --- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java @@ -16,7 +16,6 @@  package com.android.intentresolver.chooser; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.prediction.AppTarget;  import android.content.ComponentName; @@ -33,6 +32,8 @@ import android.text.SpannableStringBuilder;  import android.util.HashedStringCache;  import android.util.Log; +import androidx.annotation.Nullable; +  import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;  import java.util.ArrayList; diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java index 9d793994..ba6c3c05 100644 --- a/java/src/com/android/intentresolver/chooser/TargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java @@ -17,14 +17,15 @@  package com.android.intentresolver.chooser; -import android.annotation.Nullable;  import android.app.Activity;  import android.app.prediction.AppTarget;  import android.content.ComponentName;  import android.content.Context;  import android.content.Intent; +import android.content.SharedPreferences;  import android.content.pm.ResolveInfo;  import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager;  import android.graphics.drawable.Drawable;  import android.os.Bundle;  import android.os.UserHandle; @@ -32,6 +33,12 @@ import android.service.chooser.ChooserTarget;  import android.text.TextUtils;  import android.util.HashedStringCache; +import androidx.annotation.Nullable; + +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.ChooserRefinementManager; +import com.android.intentresolver.ResolverActivity; +  import java.util.ArrayList;  import java.util.List;  import java.util.Objects; @@ -187,9 +194,9 @@ public interface TargetInfo {       * Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager}       * received from the caller's refinement flow. This may succeed only if the target has a source       * intent that matches the filtering parameters of the proposed refinement (according to -     * {@link Intent#filterEquals()}). Then the first such match is the "base intent," and the -     * proposed refinement is merged into that base (via {@link Intent#fillIn()}; this can never -     * result in a change to the {@link Intent#filterEquals()} status of the base, but may e.g. add +     * {@link Intent#filterEquals}). Then the first such match is the "base intent," and the +     * proposed refinement is merged into that base (via {@link Intent#fillIn}; this can never +     * result in a change to the {@link Intent#filterEquals} status of the base, but may e.g. add       * new "extras" that weren't previously given in the base intent).       *       * @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of @@ -280,7 +287,7 @@ public interface TargetInfo {      }      /** -     * @return the {@link ShortcutManager} data for any shortcut associated with this target. +     * @return the {@link ShortcutInfo} for any shortcut associated with this target.       */      @Nullable      default ShortcutInfo getDirectShareShortcutInfo() { @@ -422,7 +429,7 @@ public interface TargetInfo {      /**       * @return true if this target should be logged with the "direct_share" metrics category in -     * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch()}. This is defined for legacy +     * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch}. This is defined for legacy       * compatibility and is <em>not</em> likely to be a good indicator of whether this is actually a       * "direct share" target (e.g. because it historically also applies to "empty" and "placeholder"       * targets). diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt index 103e8bf4..10ee5af1 100644 --- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt @@ -16,6 +16,7 @@  package com.android.intentresolver.contentpreview +import android.content.Intent  import androidx.annotation.MainThread  import androidx.lifecycle.ViewModel  import com.android.intentresolver.ChooserRequestParameters @@ -24,7 +25,7 @@ import com.android.intentresolver.ChooserRequestParameters  abstract class BasePreviewViewModel : ViewModel() {      @MainThread      abstract fun createOrReuseProvider( -        chooserRequest: ChooserRequestParameters +        targetIntent: Intent      ): PreviewDataProvider      @MainThread abstract fun createOrReuseImageLoader(): ImageLoader diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index d279f11f..a015147d 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -16,8 +16,6 @@  package com.android.intentresolver.contentpreview; -import static androidx.lifecycle.LifecycleKt.getCoroutineScope; -  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_TEXT; @@ -28,11 +26,11 @@ import android.content.res.Resources;  import android.net.Uri;  import android.text.TextUtils;  import android.view.LayoutInflater; +import android.view.View;  import android.view.ViewGroup;  import androidx.annotation.Nullable;  import androidx.annotation.VisibleForTesting; -import androidx.lifecycle.Lifecycle;  import com.android.intentresolver.widget.ActionRow;  import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; @@ -40,6 +38,8 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu  import java.util.List;  import java.util.function.Consumer; +import kotlinx.coroutines.CoroutineScope; +  /**   * Collection of helpers for building the content preview UI displayed in   * {@link com.android.intentresolver.ChooserActivity}. @@ -47,7 +47,7 @@ import java.util.function.Consumer;   */  public final class ChooserContentPreviewUi { -    private final Lifecycle mLifecycle; +    private final CoroutineScope mScope;      /**       * Delegate to build the default system action buttons to display in the preview layout, if/when @@ -92,14 +92,14 @@ public final class ChooserContentPreviewUi {      final ContentPreviewUi mContentPreviewUi;      public ChooserContentPreviewUi( -            Lifecycle lifecycle, +            CoroutineScope scope,              PreviewDataProvider previewData,              Intent targetIntent,              ImageLoader imageLoader,              ActionFactory actionFactory,              TransitionElementStatusCallback transitionElementStatusCallback,              HeadlineGenerator headlineGenerator) { -        mLifecycle = lifecycle; +        mScope = scope;          mContentPreviewUi = createContentPreview(                  previewData,                  targetIntent, @@ -125,7 +125,7 @@ public final class ChooserContentPreviewUi {          int previewType = previewData.getPreviewType();          if (previewType == CONTENT_PREVIEW_TEXT) {              return createTextPreview( -                    mLifecycle, +                    mScope,                      targetIntent,                      actionFactory,                      imageLoader, @@ -137,8 +137,7 @@ public final class ChooserContentPreviewUi {                      actionFactory,                      headlineGenerator);              if (previewData.getUriCount() > 0) { -                previewData.getFirstFileName( -                        mLifecycle, fileContentPreviewUi::setFirstFileName); +                previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName);              }              return fileContentPreviewUi;          } @@ -148,7 +147,7 @@ public final class ChooserContentPreviewUi {          if (!TextUtils.isEmpty(text)) {              FilesPlusTextContentPreviewUi previewUi =                      new FilesPlusTextContentPreviewUi( -                            mLifecycle, +                            mScope,                              isSingleImageShare,                              previewData.getUriCount(),                              targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), @@ -159,7 +158,7 @@ public final class ChooserContentPreviewUi {                              headlineGenerator);              if (previewData.getUriCount() > 0) {                  JavaFlowHelper.collectToList( -                        getCoroutineScope(mLifecycle), +                        mScope,                          previewData.getImagePreviewFileInfoFlow(),                          previewUi::updatePreviewMetadata);              } @@ -167,7 +166,7 @@ public final class ChooserContentPreviewUi {          }          return new UnifiedContentPreviewUi( -                getCoroutineScope(mLifecycle), +                mScope,                  isSingleImageShare,                  targetIntent.getType(),                  actionFactory, @@ -188,19 +187,22 @@ public final class ChooserContentPreviewUi {       * specified {@code intent}.       */      public ViewGroup displayContentPreview( -            Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) { -        return mContentPreviewUi.display(resources, layoutInflater, parent); +        return mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent);      }      private static TextContentPreviewUi createTextPreview( -            Lifecycle lifecycle, +            CoroutineScope scope,              Intent targetIntent,              ChooserContentPreviewUi.ActionFactory actionFactory,              ImageLoader imageLoader,              HeadlineGenerator headlineGenerator) {          CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); -        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); +        CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE);          ClipData previewData = targetIntent.getClipData();          Uri previewThumbnail = null;          if (previewData != null) { @@ -210,7 +212,7 @@ public final class ChooserContentPreviewUi {              }          }          return new TextContentPreviewUi( -                lifecycle, +                scope,                  sharingText,                  previewTitle,                  previewThumbnail, diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java index ebab147d..ad1c6c01 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java @@ -18,7 +18,7 @@ package com.android.intentresolver.contentpreview;  import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.annotation.IntDef; +import androidx.annotation.IntDef;  import java.lang.annotation.Retention; diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 2d81794e..dce146b0 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -24,10 +24,13 @@ import android.text.TextUtils;  import android.view.LayoutInflater;  import android.view.View;  import android.view.ViewGroup; +import android.view.ViewStub;  import android.view.animation.DecelerateInterpolator;  import android.widget.ImageView;  import android.widget.TextView; +import androidx.annotation.Nullable; +  import com.android.intentresolver.R;  import com.android.intentresolver.widget.ActionRow;  import com.android.intentresolver.widget.ScrollableImagePreviewView; @@ -40,7 +43,10 @@ abstract class ContentPreviewUi {      public abstract int getType();      public abstract ViewGroup display( -            Resources resources, LayoutInflater layoutInflater, ViewGroup parent); +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent);      protected static void updateViewWithImage(ImageView imageView, Bitmap image) {          if (image == null) { @@ -57,23 +63,28 @@ abstract class ContentPreviewUi {          fadeAnim.start();      } -    protected static void displayHeadline(ViewGroup layout, String headline) { -        if (layout != null) { -            TextView titleView = layout.findViewById(R.id.headline); -            if (titleView != null) { -                if (!TextUtils.isEmpty(headline)) { -                    titleView.setText(headline); -                    titleView.setVisibility(View.VISIBLE); -                } else { -                    titleView.setVisibility(View.GONE); -                } -            } +    protected static void inflateHeadline(View layout) { +        ViewStub stub = layout.findViewById(R.id.chooser_headline_row_stub); +        if (stub != null) { +            stub.inflate(); +        } +    } + +    protected static void displayHeadline(View layout, String headline) { +        TextView titleView = layout == null ? null : layout.findViewById(R.id.headline); +        if (titleView == null) { +            return; +        } +        if (!TextUtils.isEmpty(headline)) { +            titleView.setText(headline); +            titleView.setVisibility(View.VISIBLE); +        } else { +            titleView.setVisibility(View.GONE);          }      }      protected static void displayModifyShareAction( -            ViewGroup layout, -            ChooserContentPreviewUi.ActionFactory actionFactory) { +            View layout, ChooserContentPreviewUi.ActionFactory actionFactory) {          ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction();          if (modifyShareAction != null && layout != null) {              TextView modifyShareView = layout.findViewById(R.id.reselection_action); diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 20758189..89e7e528 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -67,18 +67,30 @@ class FileContentPreviewUi extends ContentPreviewUi {      }      @Override -    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { -        ViewGroup layout = displayInternal(resources, layoutInflater, parent); -        displayModifyShareAction(layout, mActionFactory); +    public ViewGroup display( +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) { +        ViewGroup layout = displayInternal(resources, layoutInflater, parent, headlineViewParent); +        displayModifyShareAction( +                headlineViewParent == null ? layout : headlineViewParent, mActionFactory);          return layout;      }      private ViewGroup displayInternal( -            Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) {          mContentPreview = (ViewGroup) layoutInflater.inflate(                  R.layout.chooser_grid_preview_file, parent, false); +        if (headlineViewParent == null) { +            headlineViewParent = mContentPreview; +        } +        inflateHeadline(headlineViewParent); -        displayHeadline(mContentPreview, mHeadlineGenerator.getFilesHeadline(mFileCount)); +        displayHeadline(headlineViewParent, mHeadlineGenerator.getFilesHeadline(mFileCount));          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 6e1212e9..78fc6586 100644 --- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -31,7 +31,6 @@ import android.widget.ImageView;  import android.widget.TextView;  import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle;  import com.android.intentresolver.R;  import com.android.intentresolver.widget.ActionRow; @@ -41,6 +40,8 @@ import java.util.HashMap;  import java.util.List;  import java.util.function.Consumer; +import kotlinx.coroutines.CoroutineScope; +  /**   * FilesPlusTextContentPreviewUi is shown when the user is sending 1 or more files along with   * non-empty EXTRA_TEXT. The text can be toggled with a checkbox. If a single image file is being @@ -48,7 +49,7 @@ import java.util.function.Consumer;   * file content).   */  class FilesPlusTextContentPreviewUi extends ContentPreviewUi { -    private final Lifecycle mLifecycle; +    private final CoroutineScope mScope;      @Nullable      private final String mIntentMimeType;      private final CharSequence mText; @@ -59,6 +60,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {      private final boolean mIsSingleImage;      private final int mFileCount;      private ViewGroup mContentPreviewView; +    private View mHeadliveView;      private boolean mIsMetadataUpdated = false;      @Nullable      private Uri mFirstFilePreviewUri; @@ -68,7 +70,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {      private static final boolean SHOW_TOGGLE_CHECKMARK = false;      FilesPlusTextContentPreviewUi( -            Lifecycle lifecycle, +            CoroutineScope scope,              boolean isSingleImage,              int fileCount,              CharSequence text, @@ -81,7 +83,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {              throw new IllegalArgumentException(                      "fileCount = " + fileCount + " and isSingleImage = true");          } -        mLifecycle = lifecycle; +        mScope = scope;          mIntentMimeType = intentMimeType;          mFileCount = fileCount;          mIsSingleImage = isSingleImage; @@ -98,9 +100,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {      }      @Override -    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { -        ViewGroup layout = displayInternal(layoutInflater, parent); -        displayModifyShareAction(layout, mActionFactory); +    public ViewGroup display( +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) { +        ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent); +        displayModifyShareAction( +                headlineViewParent == null ? layout : headlineViewParent, mActionFactory);          return layout;      } @@ -118,13 +125,18 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {          mFirstFilePreviewUri = files.isEmpty() ? null : files.get(0).getPreviewUri();          mIsMetadataUpdated = true;          if (mContentPreviewView != null) { -            updateUiWithMetadata(mContentPreviewView); +            updateUiWithMetadata(mContentPreviewView, mHeadliveView);          }      } -    private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { +    private ViewGroup displayInternal( +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) {          mContentPreviewView = (ViewGroup) layoutInflater.inflate(                  R.layout.chooser_grid_preview_files_text, parent, false); +        mHeadliveView = headlineViewParent == null ? mContentPreviewView : headlineViewParent; +        inflateHeadline(mHeadliveView);          final ActionRow actionRow =                  mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row); @@ -134,12 +146,12 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {          if (!mIsSingleImage) {              mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);          } -        prepareTextPreview(mContentPreviewView, mActionFactory); +        prepareTextPreview(mContentPreviewView, mHeadliveView, mActionFactory);          if (mIsMetadataUpdated) { -            updateUiWithMetadata(mContentPreviewView); +            updateUiWithMetadata(mContentPreviewView, mHeadliveView);          } else {              updateHeadline( -                    mContentPreviewView, +                    mHeadliveView,                      mFileCount,                      mTypeClassifier.isImageType(mIntentMimeType),                      mTypeClassifier.isVideoType(mIntentMimeType)); @@ -148,13 +160,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {          return mContentPreviewView;      } -    private void updateUiWithMetadata(ViewGroup contentPreviewView) { -        updateHeadline(contentPreviewView, mFileCount, mAllImages, mAllVideos); +    private void updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView) { +        prepareTextPreview(contentPreviewView, headlineView, mActionFactory); +        updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);          ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);          if (mIsSingleImage && mFirstFilePreviewUri != null) {              mImageLoader.loadImage( -                    mLifecycle, +                    mScope,                      mFirstFilePreviewUri,                      bitmap -> {                          if (bitmap == null) { @@ -169,8 +182,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {      }      private void updateHeadline( -            ViewGroup contentPreview, int fileCount, boolean allImages, boolean allVideos) { -        CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action); +            View headlineView, int fileCount, boolean allImages, boolean allVideos) { +        CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);          String headline;          if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {              if (allImages) { @@ -190,14 +203,15 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {              }          } -        displayHeadline(contentPreview, headline); +        displayHeadline(headlineView, headline);      }      private void prepareTextPreview(              ViewGroup contentPreview, +            View headlineView,              ChooserContentPreviewUi.ActionFactory actionFactory) {          final TextView textView = contentPreview.requireViewById(R.id.content_preview_text); -        CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action); +        CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);          boolean isLink = HttpUriMatcher.isHttpUri(mText.toString());          textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);          textView.setText(mText); @@ -213,7 +227,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {                  textView.setText(getNoTextString(contentPreview.getResources()));              }              shareTextAction.accept(!isChecked); -            updateHeadline(contentPreview, mFileCount, mAllImages, mAllVideos); +            updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);          });          if (SHOW_TOGGLE_CHECKMARK) {              includeText.setVisibility(View.VISIBLE); diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt index 1aace8c3..ef1e55d8 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -16,36 +16,55 @@  package com.android.intentresolver.contentpreview -import android.annotation.StringRes  import android.content.Context -import com.android.intentresolver.R  import android.util.PluralsMessageFormatter +import androidx.annotation.StringRes +import com.android.intentresolver.R  private const val PLURALS_COUNT = "count"  /** - * 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.   */  class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {      override fun getTextHeadline(text: CharSequence): String {          return context.getString( -            getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)) +            getTemplateResource(text, R.string.sharing_link, R.string.sharing_text) +        )      }      override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String { -        return getPluralString(getTemplateResource( -            text, R.string.sharing_images_with_link, R.string.sharing_images_with_text), count) +        return getPluralString( +            getTemplateResource( +                text, +                R.string.sharing_images_with_link, +                R.string.sharing_images_with_text +            ), +            count +        )      }      override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String { -        return getPluralString(getTemplateResource( -            text, R.string.sharing_videos_with_link, R.string.sharing_videos_with_text), count) +        return getPluralString( +            getTemplateResource( +                text, +                R.string.sharing_videos_with_link, +                R.string.sharing_videos_with_text +            ), +            count +        )      }      override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String { -        return getPluralString(getTemplateResource( -            text, R.string.sharing_files_with_link, R.string.sharing_files_with_text), count) +        return getPluralString( +            getTemplateResource( +                text, +                R.string.sharing_files_with_link, +                R.string.sharing_files_with_text +            ), +            count +        )      }      override fun getImagesHeadline(count: Int): String { @@ -70,7 +89,9 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {      @StringRes      private fun getTemplateResource( -        text: CharSequence, @StringRes linkResource: Int, @StringRes nonLinkResource: Int +        text: CharSequence, +        @StringRes linkResource: Int, +        @StringRes nonLinkResource: Int      ): Int {          return if (text.toString().isHttpUri()) linkResource else nonLinkResource      } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt index 8d0fb84b..629651a3 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -18,8 +18,8 @@ package com.android.intentresolver.contentpreview  import android.graphics.Bitmap  import android.net.Uri -import androidx.lifecycle.Lifecycle  import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope  /** A content preview image loader. */  interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? { @@ -30,7 +30,7 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm       * @param callback a callback that will be invoked with the loaded image or null if loading has       *   failed.       */ -    fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) +    fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>)      /** Prepopulate the image loader cache. */      fun prePopulate(uris: List<Uri>) diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt index 22dd1125..572ccf0b 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt @@ -24,8 +24,6 @@ import android.util.Size  import androidx.annotation.GuardedBy  import androidx.annotation.VisibleForTesting  import androidx.collection.LruCache -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope  import java.util.function.Consumer  import kotlinx.coroutines.CancellationException  import kotlinx.coroutines.CompletableDeferred @@ -70,8 +68,8 @@ constructor(      override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) -    override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) { -        callerLifecycle.coroutineScope.launch { +    override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) { +        callerScope.launch {              val image = loadImageAsync(uri, caching = true)              if (isActive) {                  callback.accept(image) diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt index 90016932..31a7006c 100644 --- a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt +++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt @@ -19,13 +19,17 @@ package com.android.intentresolver.contentpreview  import android.content.res.Resources  import android.util.Log  import android.view.LayoutInflater +import android.view.View  import android.view.ViewGroup  internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {      override fun getType(): Int = type      override fun display( -        resources: Resources?, layoutInflater: LayoutInflater?, parent: ViewGroup? +        resources: Resources?, +        layoutInflater: LayoutInflater?, +        parent: ViewGroup?, +        headlineViewParent: View?,      ): ViewGroup? {          Log.e(TAG, "Unexpected content preview type: $type")          return null diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt index 9f1cc6c1..38918d79 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -29,8 +29,6 @@ import android.text.TextUtils  import android.util.Log  import androidx.annotation.OpenForTesting  import androidx.annotation.VisibleForTesting -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope  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_TEXT @@ -185,11 +183,11 @@ constructor(       * is not provided, derived from the URI.       */      @Throws(IndexOutOfBoundsException::class) -    fun getFirstFileName(callerLifecycle: Lifecycle, callback: Consumer<String>) { +    fun getFirstFileName(callerScope: CoroutineScope, callback: Consumer<String>) {          if (records.isEmpty()) {              throw IndexOutOfBoundsException("There are no shared URIs")          } -        callerLifecycle.coroutineScope.launch { +        callerScope.launch {              val result = scope.async { getFirstFileName() }.await()              callback.accept(result)          } @@ -264,44 +262,46 @@ constructor(          private val query by lazy { readQueryResult() } -        private fun readQueryResult(): QueryResult { -            val cursor = -                contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult() - -            var flagColIdx = -1 -            var displayIconUriColIdx = -1 -            var nameColIndex = -1 -            var titleColIndex = -1 -            // TODO: double-check why Cursor#getColumnInded didn't work -            cursor.columnNames.forEachIndexed { i, columnName -> -                when (columnName) { -                    DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i -                    MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i -                    OpenableColumns.DISPLAY_NAME -> nameColIndex = i -                    Downloads.Impl.COLUMN_TITLE -> titleColIndex = i +        private fun readQueryResult(): QueryResult = +            contentResolver.querySafe(uri)?.use { cursor -> +                if (!cursor.moveToFirst()) return@use null + +                var flagColIdx = -1 +                var displayIconUriColIdx = -1 +                var nameColIndex = -1 +                var titleColIndex = -1 +                // TODO: double-check why Cursor#getColumnInded didn't work +                cursor.columnNames.forEachIndexed { i, columnName -> +                    when (columnName) { +                        DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i +                        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i +                        OpenableColumns.DISPLAY_NAME -> nameColIndex = i +                        Downloads.Impl.COLUMN_TITLE -> titleColIndex = i +                    }                  } -            } - -            val supportsThumbnail = -                flagColIdx >= 0 && ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0) -            var title = "" -            if (nameColIndex >= 0) { -                title = cursor.getString(nameColIndex) ?: "" -            } -            if (TextUtils.isEmpty(title) && titleColIndex >= 0) { -                title = cursor.getString(titleColIndex) ?: "" -            } +                val supportsThumbnail = +                    flagColIdx >= 0 && +                        ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0) -            val iconUri = -                if (displayIconUriColIdx >= 0) { -                    cursor.getString(displayIconUriColIdx)?.let(Uri::parse) -                } else { -                    null +                var title = "" +                if (nameColIndex >= 0) { +                    title = cursor.getString(nameColIndex) ?: "" +                } +                if (TextUtils.isEmpty(title) && titleColIndex >= 0) { +                    title = cursor.getString(titleColIndex) ?: ""                  } -            return QueryResult(supportsThumbnail, title, iconUri) -        } +                val iconUri = +                    if (displayIconUriColIdx >= 0) { +                        cursor.getString(displayIconUriColIdx)?.let(Uri::parse) +                    } else { +                        null +                    } + +                QueryResult(supportsThumbnail, title, iconUri) +            } +                ?: QueryResult()      }      private class QueryResult( diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 6013f5a0..6350756e 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -17,6 +17,7 @@  package com.android.intentresolver.contentpreview  import android.app.Application +import android.content.Intent  import androidx.annotation.MainThread  import androidx.lifecycle.ViewModel  import androidx.lifecycle.ViewModelProvider @@ -25,26 +26,32 @@ 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 kotlinx.coroutines.CoroutineDispatcher  import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.plus  /** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */ -class PreviewViewModel( +@HiltViewModel +class PreviewViewModel +@Inject +constructor(      private val application: Application, -    private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +    @Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,  ) : BasePreviewViewModel() {      private var previewDataProvider: PreviewDataProvider? = null      private var imageLoader: ImagePreviewImageLoader? = null      @MainThread      override fun createOrReuseProvider( -        chooserRequest: ChooserRequestParameters +        targetIntent: Intent      ): PreviewDataProvider =          previewDataProvider              ?: PreviewDataProvider(                      viewModelScope + dispatcher, -                    chooserRequest.targetIntent, +                    targetIntent,                      application.contentResolver                  )                  .also { previewDataProvider = it } diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index c38ed03a..b0dc3c58 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -20,6 +20,7 @@ import static com.android.intentresolver.util.UriFilters.isOwnedByCurrentUser;  import android.content.res.Resources;  import android.net.Uri; +import android.text.SpannableStringBuilder;  import android.text.TextUtils;  import android.view.LayoutInflater;  import android.view.View; @@ -28,13 +29,14 @@ import android.widget.ImageView;  import android.widget.TextView;  import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle;  import com.android.intentresolver.R;  import com.android.intentresolver.widget.ActionRow; +import kotlinx.coroutines.CoroutineScope; +  class TextContentPreviewUi extends ContentPreviewUi { -    private final Lifecycle mLifecycle; +    private final CoroutineScope mScope;      @Nullable      private final CharSequence mSharingText;      @Nullable @@ -46,14 +48,14 @@ class TextContentPreviewUi extends ContentPreviewUi {      private final HeadlineGenerator mHeadlineGenerator;      TextContentPreviewUi( -            Lifecycle lifecycle, +            CoroutineScope scope,              @Nullable CharSequence sharingText,              @Nullable CharSequence previewTitle,              @Nullable Uri previewThumbnail,              ChooserContentPreviewUi.ActionFactory actionFactory,              ImageLoader imageLoader,              HeadlineGenerator headlineGenerator) { -        mLifecycle = lifecycle; +        mScope = scope;          mSharingText = sharingText;          mPreviewTitle = previewTitle;          mPreviewThumbnail = previewThumbnail; @@ -68,17 +70,27 @@ class TextContentPreviewUi extends ContentPreviewUi {      }      @Override -    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { -        ViewGroup layout = displayInternal(layoutInflater, parent); -        displayModifyShareAction(layout, mActionFactory); +    public ViewGroup display( +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) { +        ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent); +        displayModifyShareAction( +                headlineViewParent == null ? layout : headlineViewParent, mActionFactory);          return layout;      }      private ViewGroup displayInternal(              LayoutInflater layoutInflater, -            ViewGroup parent) { +            ViewGroup parent, +            @Nullable View headlineViewParent) {          ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(                  R.layout.chooser_grid_preview_text, parent, false); +        if (headlineViewParent == null) { +            headlineViewParent = contentPreviewLayout; +        } +        inflateHeadline(headlineViewParent);          final ActionRow actionRow =                  contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); @@ -93,13 +105,9 @@ class TextContentPreviewUi extends ContentPreviewUi {          TextView textView = contentPreviewLayout.findViewById(                  com.android.internal.R.id.content_preview_text); -        String text = mSharingText.toString(); -        // If we're only previewing one line, then strip out newlines. -        if (textView.getMaxLines() == 1) { -            text = text.replace("\n", " "); -        } -        textView.setText(text); +        textView.setText( +                textView.getMaxLines() == 1 ? replaceLineBreaks(mSharingText) : mSharingText);          TextView previewTitleView = contentPreviewLayout.findViewById(                  com.android.internal.R.id.content_preview_title); @@ -115,7 +123,7 @@ class TextContentPreviewUi extends ContentPreviewUi {              previewThumbnailView.setVisibility(View.GONE);          } else {              mImageLoader.loadImage( -                    mLifecycle, +                    mScope,                      mPreviewThumbnail,                      (bitmap) -> updateViewWithImage(                              contentPreviewLayout.findViewById( @@ -131,8 +139,22 @@ class TextContentPreviewUi extends ContentPreviewUi {              copyButton.setVisibility(View.GONE);          } -        displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText)); +        displayHeadline(headlineViewParent, mHeadlineGenerator.getTextHeadline(mSharingText));          return contentPreviewLayout;      } + +    @Nullable +    private static CharSequence replaceLineBreaks(@Nullable CharSequence text) { +        if (text == null) { +            return null; +        } +        SpannableStringBuilder string = new SpannableStringBuilder(text); +        for (int i = 0, size = string.length(); i < size; i++) { +            if (string.charAt(i) == '\n') { +                string.replace(i, i + 1, " "); +            } +        } +        return string; +    }  } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 8e635aba..8ddd5273 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -52,6 +52,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {      private List<FileInfo> mFiles;      @Nullable      private ViewGroup mContentPreviewView; +    @Nullable +    private View mHeadlineView;      UnifiedContentPreviewUi(              CoroutineScope scope, @@ -83,9 +85,14 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {      }      @Override -    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { -        ViewGroup layout = displayInternal(layoutInflater, parent); -        displayModifyShareAction(layout, mActionFactory); +    public ViewGroup display( +            Resources resources, +            LayoutInflater layoutInflater, +            ViewGroup parent, +            @Nullable View headlineViewParent) { +        ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent); +        displayModifyShareAction( +                headlineViewParent == null ? layout : headlineViewParent, mActionFactory);          return layout;      } @@ -96,13 +103,16 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {                  .toList());          mFiles = files;          if (mContentPreviewView != null) { -            updatePreviewWithFiles(mContentPreviewView, files); +            updatePreviewWithFiles(mContentPreviewView, mHeadlineView, files);          }      } -    private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { +    private ViewGroup displayInternal( +            LayoutInflater layoutInflater, ViewGroup parent, @Nullable View headlineViewParent) {          mContentPreviewView = (ViewGroup) layoutInflater.inflate(                  R.layout.chooser_grid_preview_image, parent, false); +        mHeadlineView = headlineViewParent == null ? mContentPreviewView : headlineViewParent; +        inflateHeadline(mHeadlineView);          final ActionRow actionRow =                  mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row); @@ -122,10 +132,10 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {                  mItemCount);          if (mFiles != null) { -            updatePreviewWithFiles(mContentPreviewView, mFiles); +            updatePreviewWithFiles(mContentPreviewView, mHeadlineView, mFiles);          } else {              displayHeadline( -                    mContentPreviewView, +                    mHeadlineView,                      mItemCount,                      mTypeClassifier.isImageType(mIntentMimeType),                      mTypeClassifier.isVideoType(mIntentMimeType)); @@ -135,7 +145,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {          return mContentPreviewView;      } -    private void updatePreviewWithFiles(ViewGroup contentPreviewView, List<FileInfo> files) { +    private void updatePreviewWithFiles( +            ViewGroup contentPreviewView, View headlineView, List<FileInfo> files) {          final int count = files.size();          ScrollableImagePreviewView imagePreview =                  contentPreviewView.requireViewById(R.id.scrollable_image_preview); @@ -158,11 +169,11 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {              allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;          } -        displayHeadline(contentPreviewView, count, allImages, allVideos); +        displayHeadline(headlineView, count, allImages, allVideos);      }      private void displayHeadline( -            ViewGroup layout, int count, boolean allImages, boolean allVideos) { +            View layout, int count, boolean allImages, boolean allVideos) {          if (allImages) {              displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));          } else if (allVideos) { diff --git a/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java new file mode 100644 index 00000000..41422b66 --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java @@ -0,0 +1,46 @@ +/* + * 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.emptystate; + +import android.annotation.Nullable; + +import com.android.intentresolver.ResolverListAdapter; + +/** + * Empty state provider that combines multiple providers. Providers earlier in the list have + * priority, that is if there is a provider that returns non-null empty state then all further + * providers will be ignored. + */ +public class CompositeEmptyStateProvider implements EmptyStateProvider { + +    private final EmptyStateProvider[] mProviders; + +    public CompositeEmptyStateProvider(EmptyStateProvider... providers) { +        mProviders = providers; +    } + +    @Nullable +    @Override +    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { +        for (EmptyStateProvider provider : mProviders) { +            EmptyState emptyState = provider.getEmptyState(resolverListAdapter); +            if (emptyState != null) { +                return emptyState; +            } +        } +        return null; +    } +} diff --git a/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java new file mode 100644 index 00000000..2164e533 --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java @@ -0,0 +1,59 @@ +/* + * 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.emptystate; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.IPackageManager; + +import com.android.intentresolver.IntentForwarderActivity; + +import java.util.List; + +/** + * Utility class to check if there are cross profile intents, it is in a separate class so + * it could be mocked in tests + */ +public class CrossProfileIntentsChecker { + +    private final ContentResolver mContentResolver; +    private final IPackageManager mPackageManager; + +    public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) { +        this(contentResolver, AppGlobals.getPackageManager()); +    } + +    CrossProfileIntentsChecker( +            @NonNull ContentResolver contentResolver, IPackageManager packageManager) { +        mContentResolver = contentResolver; +        mPackageManager = packageManager; +    } + +    /** +     * Returns {@code true} if at least one of the provided {@code intents} can be forwarded +     * from {@code source} (user id) to {@code target} (user id). +     */ +    public boolean hasCrossProfileIntents( +            List<Intent> intents, @UserIdInt int source, @UserIdInt int target) { +        return intents.stream().anyMatch(intent -> +                null != IntentForwarderActivity.canForward(intent, source, target, +                        mPackageManager, mContentResolver)); +    } +} + diff --git a/java/src/com/android/intentresolver/emptystate/EmptyState.java b/java/src/com/android/intentresolver/emptystate/EmptyState.java new file mode 100644 index 00000000..cde99fe1 --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/EmptyState.java @@ -0,0 +1,78 @@ +/* + * 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.emptystate; + +import android.annotation.Nullable; + +/** + * Model for the "empty state"/"blocker" UI to display instead of a profile tab's normal contents. + */ +public interface EmptyState { +    /** +     * Get the title to show on the empty state. +     */ +    @Nullable +    default String getTitle() { +        return null; +    } + +    /** +     * Get the subtitle string to show underneath the title on the empty state. +     */ +    @Nullable +    default String getSubtitle()  { +        return null; +    } + +    /** +     * Get the handler for an optional button associated with this empty state. If the result is +     * non-null, the empty-state UI will be built with a button that dispatches this handler. +     */ +    @Nullable +    default ClickListener getButtonClickListener()  { +        return null; +    } + +    /** +     * Get whether to show the default UI for the empty state. If true, the UI will show the default +     * blocker text ('No apps can perform this action') and style; title and subtitle are ignored. +     */ +    default boolean useDefaultEmptyView() { +        return false; +    } + +    /** +     * Returns true if for this empty state we should skip rebuilding of the apps list +     * for this tab. +     */ +    default boolean shouldSkipDataRebuild() { +        return false; +    } + +    /** +     * Called when empty state is shown, could be used e.g. to track analytics events. +     */ +    default void onEmptyStateShown() {} + +    interface ClickListener { +        void onClick(TabControl currentTab); +    } + +    interface TabControl { +        void showSpinner(); +    } +} diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java new file mode 100644 index 00000000..c3261287 --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java @@ -0,0 +1,37 @@ +/* + * 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.emptystate; + +import android.annotation.Nullable; + +import com.android.intentresolver.ResolverListAdapter; + +/** + * Returns an empty state to show for the current profile page (tab) if necessary. + * This could be used e.g. to show a blocker on a tab if device management policy doesn't + * allow to use it or there are no apps available. + */ +public interface EmptyStateProvider { +    /** +     * When a non-null empty state is returned the corresponding profile page will show +     * this empty state +     * @param resolverListAdapter the current adapter +     */ +    @Nullable +    default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { +        return null; +    } +} diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java new file mode 100644 index 00000000..d7ef8c75 --- /dev/null +++ b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java @@ -0,0 +1,63 @@ +/* + * 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.emptystate; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by + * some empty-state status. + */ +public class EmptyStateUiHelper { +    private final View mEmptyStateView; + +    public EmptyStateUiHelper(ViewGroup rootView) { +        mEmptyStateView = +                rootView.requireViewById(com.android.internal.R.id.resolver_empty_state); +    } + +    public void resetViewVisibilities() { +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title) +                .setVisibility(View.VISIBLE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle) +                .setVisibility(View.VISIBLE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button) +                .setVisibility(View.INVISIBLE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress) +                .setVisibility(View.GONE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.empty) +                .setVisibility(View.GONE); +        mEmptyStateView.setVisibility(View.VISIBLE); +    } + +    public void showSpinner() { +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title) +                .setVisibility(View.INVISIBLE); +        // TODO: subtitle? +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button) +                .setVisibility(View.INVISIBLE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress) +                .setVisibility(View.VISIBLE); +        mEmptyStateView.requireViewById(com.android.internal.R.id.empty) +                .setVisibility(View.GONE); +    } + +    public void hide() { +        mEmptyStateView.setVisibility(View.GONE); +    } +} + diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java index a7b50f38..2653c560 100644 --- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -14,13 +14,11 @@   * limitations under the License.   */ -package com.android.intentresolver; +package com.android.intentresolver.emptystate;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.admin.DevicePolicyEventLogger;  import android.app.admin.DevicePolicyManager;  import android.content.Context; @@ -28,8 +26,11 @@ import android.content.pm.ResolveInfo;  import android.os.UserHandle;  import android.stats.devicepolicy.nano.DevicePolicyEnums; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.ResolverListAdapter;  import com.android.internal.R;  import java.util.List; @@ -51,9 +52,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {      @NonNull      private final UserHandle mTabOwnerUserHandleForLaunch; -    public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle, -            UserHandle personalProfileUserHandle, String metricsCategory, -            UserHandle tabOwnerUserHandleForLaunch) { +    public NoAppsAvailableEmptyStateProvider( +            @NonNull Context context, +            @Nullable UserHandle workProfileUserHandle, +            @Nullable UserHandle personalProfileUserHandle, +            @NonNull String metricsCategory, +            @NonNull UserHandle tabOwnerUserHandleForLaunch) {          mContext = context;          mWorkProfileUserHandle = workProfileUserHandle;          mPersonalProfileUserHandle = personalProfileUserHandle; @@ -76,12 +80,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {                  title = mContext.getSystemService(                          DevicePolicyManager.class).getResources().getString(                          RESOLVER_NO_PERSONAL_APPS, -                        () -> mContext.getString(R.string.resolver_no_personal_apps_available)); +                            () -> mContext.getString(R.string.resolver_no_personal_apps_available));              } else {                  title = mContext.getSystemService(                          DevicePolicyManager.class).getResources().getString(                          RESOLVER_NO_WORK_APPS, -                        () -> mContext.getString(R.string.resolver_no_work_apps_available)); +                            () -> mContext.getString(R.string.resolver_no_work_apps_available));              }              return new NoAppsAvailableEmptyState( @@ -128,8 +132,9 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {          private boolean mIsPersonalProfile; -        public NoAppsAvailableEmptyState(String title, String metricsCategory, -                boolean isPersonalProfile) { +        public NoAppsAvailableEmptyState(@NonNull String title, +                                         @NonNull String metricsCategory, +                                         boolean isPersonalProfile) {              mTitle = title;              mMetricsCategory = metricsCategory;              mIsPersonalProfile = isPersonalProfile; diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java index 6f72bb00..ce7bd8d9 100644 --- a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java @@ -14,19 +14,18 @@   * limitations under the License.   */ -package com.android.intentresolver; +package com.android.intentresolver.emptystate; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.StringRes;  import android.app.admin.DevicePolicyEventLogger;  import android.app.admin.DevicePolicyManager;  import android.content.Context;  import android.os.UserHandle; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.intentresolver.ResolverListAdapter;  /**   * Empty state provider that does not allow cross profile sharing, it will return a blocker @@ -92,10 +91,14 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {          @NonNull          private final String mEventCategory; -        public DevicePolicyBlockerEmptyState(Context context, String devicePolicyStringTitleId, -                @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId, +        public DevicePolicyBlockerEmptyState( +                @NonNull Context context, +                String devicePolicyStringTitleId, +                @StringRes int defaultTitleResource, +                String devicePolicyStringSubtitleId,                  @StringRes int defaultSubtitleResource, -                int devicePolicyEventId, String devicePolicyEventCategory) { +                int devicePolicyEventId, +                @NonNull String devicePolicyEventCategory) {              mContext = context;              mDevicePolicyStringTitleId = devicePolicyStringTitleId;              mDefaultTitleResource = defaultTitleResource; diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java index 2f3dfbd5..612828e0 100644 --- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java @@ -14,21 +14,23 @@   * limitations under the License.   */ -package com.android.intentresolver; +package com.android.intentresolver.emptystate;  import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; -import android.annotation.NonNull; -import android.annotation.Nullable;  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 com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +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;  /**   * Chooser/ResolverActivity empty state provider that returns empty state which is shown when @@ -65,7 +67,7 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {          final String title = mContext.getSystemService(DevicePolicyManager.class)                  .getResources().getString(RESOLVER_WORK_PAUSED_TITLE, -                () -> mContext.getString(R.string.resolver_turn_on_work_apps)); +                    () -> mContext.getString(R.string.resolver_turn_on_work_apps));          return new WorkProfileOffEmptyState(title, (tab) -> {              tab.showSpinner(); diff --git a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt b/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt deleted file mode 100644 index d1494fe7..00000000 --- a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.flags - -import android.provider.DeviceConfig -import com.android.systemui.flags.ParcelableFlag - -internal class DeviceConfigProxy { -    fun isEnabled(flag: ParcelableFlag<Boolean>): Boolean? { -        return runCatching { -            val hasProperty = DeviceConfig.getProperty(flag.namespace, flag.name) != null -            if (hasProperty) { -                DeviceConfig.getBoolean(flag.namespace, flag.name, flag.default) -            } else { -                null -            } -        }.getOrDefault(null) -    } -} diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt deleted file mode 100644 index 2c20d341..00000000 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.flags - -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag - -// Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to -// make the flags available in the flag flipper app (see go/sysui-flags). -// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS. -object Flags { -    private fun releasedFlag(name: String) = ReleasedFlag(name, "systemui") - -    private fun unreleasedFlag(name: String, teamfood: Boolean = false) = -        UnreleasedFlag(name, "systemui", teamfood) -} diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java index fadea934..51d4e677 100644 --- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java +++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java @@ -32,9 +32,12 @@ import android.view.animation.DecelerateInterpolator;  import android.widget.Space;  import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable;  import androidx.recyclerview.widget.RecyclerView;  import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.FeatureFlags;  import com.android.intentresolver.R;  import com.android.intentresolver.ResolverListAdapter.ViewHolder;  import com.android.internal.annotations.VisibleForTesting; @@ -107,6 +110,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      private final boolean mShouldShowContentPreview;      private final int mChooserWidthPixels;      private final int mChooserRowTextOptionTranslatePixelSize; +    private final FeatureFlags mFeatureFlags; +    @Nullable +    private RecyclerView mRecyclerView;      private int mChooserTargetWidth = 0; @@ -119,7 +125,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.              ChooserActivityDelegate chooserActivityDelegate,              ChooserListAdapter wrappedAdapter,              boolean shouldShowContentPreview, -            int maxTargetsPerRow) { +            int maxTargetsPerRow, +            FeatureFlags featureFlags) {          super();          mChooserActivityDelegate = chooserActivityDelegate; @@ -133,6 +140,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);          mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(                  R.dimen.chooser_row_text_option_translate); +        mFeatureFlags = featureFlags;          wrappedAdapter.registerDataSetObserver(new DataSetObserver() {              @Override @@ -149,6 +157,18 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          });      } +    @Override +    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { +        if (mFeatureFlags.scrollablePreview()) { +            mRecyclerView = recyclerView; +        } +    } + +    @Override +    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { +        mRecyclerView = null; +    } +      public void setFooterHeight(int height) {          mFooterHeight = height;      } @@ -198,7 +218,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.      public int getSystemRowCount() {          // For the tabbed case we show the sticky content preview above the tabs,          // please refer to shouldShowStickyContentPreview -        if (mChooserActivityDelegate.shouldShowTabs()) { +        if (mChooserActivityDelegate.shouldShowTabs() +                || mFeatureFlags.scrollablePreview()) {              return 0;          } @@ -267,8 +288,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.                  + getFooterRowCount();      } +    @NonNull      @Override -    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { +    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {          switch (viewType) {              case VIEW_TYPE_CONTENT_PREVIEW:                  return new ItemViewHolder( @@ -304,7 +326,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.                  return new FooterViewHolder(sp, viewType);              default:                  // Since we catch all possible viewTypes above, no chance this is being called. -                return null; +                throw new IllegalStateException("unmatched view type");          }      } @@ -318,6 +340,15 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.          mAzLabelVisibility = isVisible;          int azRowPos = getAzLabelRowPosition();          if (azRowPos >= 0) { +            if (mRecyclerView != null) { +                for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) { +                    View child = mRecyclerView.getChildAt(i); +                    if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) { +                        child.setVisibility(isVisible ? View.VISIBLE : View.GONE); +                    } +                } +                return; +            }              notifyItemChanged(azRowPos);          }      } diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt index 0e4d0209..054fbe71 100644 --- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt @@ -18,7 +18,6 @@ package com.android.intentresolver.icons  import android.app.ActivityManager  import android.content.Context -import android.content.pm.ResolveInfo  import android.graphics.drawable.Drawable  import android.os.AsyncTask  import android.os.UserHandle @@ -95,7 +94,7 @@ class DefaultTargetDataLoader(              .executeOnExecutor(executor)      } -    override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) { +    override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) {          val taskId = nextTaskId.getAndIncrement()          LoadLabelTask(context, info, isAudioCaptureDevice, presentationFactory) { result ->                  removeTask(taskId) @@ -105,8 +104,14 @@ class DefaultTargetDataLoader(              .executeOnExecutor(executor)      } -    override fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter = -        presentationFactory.makePresentationGetter(info) +    override fun getOrLoadLabel(info: DisplayResolveInfo) { +        if (!info.hasDisplayLabel()) { +            val result = +                LoadLabelTask.loadLabel(context, info, isAudioCaptureDevice, presentationFactory) +            info.displayLabel = result.label +            info.extendedInfo = result.subLabel +        } +    }      private fun addTask(id: Int, task: AsyncTask<*, *, *>) {          synchronized(activeTasks) { activeTasks.put(id, task) } diff --git a/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/java/src/com/android/intentresolver/icons/LabelInfo.kt index 6bf7579e..a9c4cd77 100644 --- a/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt +++ b/java/src/com/android/intentresolver/icons/LabelInfo.kt @@ -1,5 +1,5 @@  /* - * Copyright (C) 2022 The Android Open Source Project + * 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. @@ -14,11 +14,6 @@   * limitations under the License.   */ -package com.android.intentresolver.flags +package com.android.intentresolver.icons -import android.content.Context - -class FeatureFlagRepositoryFactory { -    fun create(context: Context): FeatureFlagRepository = -        ReleaseFeatureFlagRepository(DeviceConfigProxy()) -} +class LabelInfo(val label: CharSequence?, val subLabel: CharSequence?) diff --git a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java index 6aee69b5..0f135d63 100644 --- a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java +++ b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java @@ -16,7 +16,6 @@  package com.android.intentresolver.icons; -import android.annotation.Nullable;  import android.content.ComponentName;  import android.content.Context;  import android.content.pm.ActivityInfo; @@ -30,6 +29,7 @@ import android.graphics.drawable.Icon;  import android.os.Trace;  import android.util.Log; +import androidx.annotation.Nullable;  import androidx.annotation.WorkerThread;  import com.android.intentresolver.SimpleIconFactory; diff --git a/java/src/com/android/intentresolver/icons/LoadLabelTask.java b/java/src/com/android/intentresolver/icons/LoadLabelTask.java index a0867b8e..6d443f78 100644 --- a/java/src/com/android/intentresolver/icons/LoadLabelTask.java +++ b/java/src/com/android/intentresolver/icons/LoadLabelTask.java @@ -28,16 +28,16 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;  import java.util.function.Consumer; -class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { +class LoadLabelTask extends AsyncTask<Void, Void, LabelInfo> {      private final Context mContext;      private final DisplayResolveInfo mDisplayResolveInfo;      private final boolean mIsAudioCaptureDevice;      protected final TargetPresentationGetter.Factory mPresentationFactory; -    private final Consumer<CharSequence[]> mCallback; +    private final Consumer<LabelInfo> mCallback;      LoadLabelTask(Context context, DisplayResolveInfo dri,              boolean isAudioCaptureDevice, TargetPresentationGetter.Factory presentationFactory, -            Consumer<CharSequence[]> callback) { +            Consumer<LabelInfo> callback) {          mContext = context;          mDisplayResolveInfo = dri;          mIsAudioCaptureDevice = isAudioCaptureDevice; @@ -46,49 +46,52 @@ class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {      }      @Override -    protected CharSequence[] doInBackground(Void... voids) { +    protected LabelInfo doInBackground(Void... voids) {          try {              Trace.beginSection("app-label"); -            return loadLabel(); +            return loadLabel( +                    mContext, mDisplayResolveInfo, mIsAudioCaptureDevice, mPresentationFactory);          } finally {              Trace.endSection();          }      } -    private CharSequence[] loadLabel() { -        TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter( -                mDisplayResolveInfo.getResolveInfo()); +    static LabelInfo loadLabel( +            Context context, +            DisplayResolveInfo displayResolveInfo, +            boolean isAudioCaptureDevice, +            TargetPresentationGetter.Factory presentationFactory) { +        TargetPresentationGetter pg = presentationFactory.makePresentationGetter( +                displayResolveInfo.getResolveInfo()); -        if (mIsAudioCaptureDevice) { +        if (isAudioCaptureDevice) {              // This is an audio capture device, so check record permissions -            ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; +            ActivityInfo activityInfo = displayResolveInfo.getResolveInfo().activityInfo;              String packageName = activityInfo.packageName;              int uid = activityInfo.applicationInfo.uid;              boolean hasRecordPermission =                      PermissionChecker.checkPermissionForPreflight( -                            mContext, +                            context,                              android.Manifest.permission.RECORD_AUDIO, -1, uid,                              packageName)                              == android.content.pm.PackageManager.PERMISSION_GRANTED;              if (!hasRecordPermission) {                  // Doesn't have record permission, so warn the user -                return new CharSequence[]{ +                return new LabelInfo(                          pg.getLabel(), -                        mContext.getString(R.string.usb_device_resolve_prompt_warn) -                }; +                        context.getString(R.string.usb_device_resolve_prompt_warn));              }          } -        return new CharSequence[]{ +        return new LabelInfo(                  pg.getLabel(), -                pg.getSubLabel() -        }; +                pg.getSubLabel());      }      @Override -    protected void onPostExecute(CharSequence[] result) { +    protected void onPostExecute(LabelInfo result) {          mCallback.accept(result);      }  } diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt index 50f731f8..07c62177 100644 --- a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt @@ -16,10 +16,8 @@  package com.android.intentresolver.icons -import android.content.pm.ResolveInfo  import android.graphics.drawable.Drawable  import android.os.UserHandle -import com.android.intentresolver.TargetPresentationGetter  import com.android.intentresolver.chooser.DisplayResolveInfo  import com.android.intentresolver.chooser.SelectableTargetInfo  import java.util.function.Consumer @@ -41,10 +39,8 @@ abstract class TargetDataLoader {      )      /** Load target label */ -    abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) +    abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) -    /** Create a presentation getter to be used with a [DisplayResolveInfo] */ -    // TODO: get rid of DisplayResolveInfo's dependency on the presentation getter and remove this -    //  method. -    abstract fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter +    /** Loads DisplayResolveInfo's display label synchronously, if needed */ +    abstract fun getOrLoadLabel(info: DisplayResolveInfo)  } diff --git a/java/src/com/android/intentresolver/inject/ActivityModule.kt b/java/src/com/android/intentresolver/inject/ActivityModule.kt new file mode 100644 index 00000000..21bfe4c6 --- /dev/null +++ b/java/src/com/android/intentresolver/inject/ActivityModule.kt @@ -0,0 +1,46 @@ +/* + * 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.Activity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import kotlinx.coroutines.CoroutineScope + +@Module +@InstallIn(ActivityComponent::class) +object ActivityModule { + +    @Provides +    @ActivityOwned +    fun lifecycle(activity: Activity): Lifecycle { +        check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" } +        return activity.lifecycle +    } + +    @Provides +    @ActivityOwned +    fun activityScope(activity: Activity): CoroutineScope { +        check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" } +        return activity.lifecycleScope +    } +} diff --git a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt new file mode 100644 index 00000000..e0f8e88b --- /dev/null +++ b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt @@ -0,0 +1,43 @@ +/* + * 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 dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +@Module +@InstallIn(SingletonComponent::class) +object ConcurrencyModule { + +    @Provides @Main fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate + +    /** Injectable alternative to [MainScope()][kotlinx.coroutines.MainScope] */ +    @Provides +    @Singleton +    @Main +    fun mainCoroutineScope(@Main mainDispatcher: CoroutineDispatcher) = +        CoroutineScope(SupervisorJob() + mainDispatcher) + +    @Provides @Background fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO +} diff --git a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt new file mode 100644 index 00000000..05cf2104 --- /dev/null +++ b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt @@ -0,0 +1,15 @@ +package com.android.intentresolver.inject + +import com.android.intentresolver.FeatureFlags +import com.android.intentresolver.FeatureFlagsImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object FeatureFlagsModule { + +    @Provides fun featureFlags(): FeatureFlags = FeatureFlagsImpl() +} diff --git a/java/src/com/android/intentresolver/inject/FrameworkModule.kt b/java/src/com/android/intentresolver/inject/FrameworkModule.kt new file mode 100644 index 00000000..2f6cc6a0 --- /dev/null +++ b/java/src/com/android/intentresolver/inject/FrameworkModule.kt @@ -0,0 +1,76 @@ +/* + * 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/Qualifiers.kt b/java/src/com/android/intentresolver/inject/Qualifiers.kt new file mode 100644 index 00000000..157e8f76 --- /dev/null +++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt @@ -0,0 +1,39 @@ +/* + * 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 javax.inject.Qualifier + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ActivityOwned + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class ApplicationOwned + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class ApplicationUser + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ProfileParent + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Default + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main diff --git a/java/src/com/android/intentresolver/inject/SingletonModule.kt b/java/src/com/android/intentresolver/inject/SingletonModule.kt new file mode 100644 index 00000000..e517800d --- /dev/null +++ b/java/src/com/android/intentresolver/inject/SingletonModule.kt @@ -0,0 +1,22 @@ +package com.android.intentresolver.inject + +import android.content.Context +import com.android.intentresolver.logging.EventLogImpl +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object SingletonModule { +    @Provides @Singleton fun instanceIdSequence() = EventLogImpl.newIdSequence() + +    @Provides +    @Reusable +    @ApplicationOwned +    fun resources(@ApplicationContext context: Context) = context.resources +} diff --git a/java/src/com/android/intentresolver/logging/EventLog.kt b/java/src/com/android/intentresolver/logging/EventLog.kt new file mode 100644 index 00000000..476bd4bf --- /dev/null +++ b/java/src/com/android/intentresolver/logging/EventLog.kt @@ -0,0 +1,74 @@ +/* + * 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.logging + +import android.net.Uri +import android.util.HashedStringCache + +/** Logs notable events during ShareSheet usage. */ +interface EventLog { + +    companion object { +        const val SELECTION_TYPE_SERVICE = 1 +        const val SELECTION_TYPE_APP = 2 +        const val SELECTION_TYPE_STANDARD = 3 +        const val SELECTION_TYPE_COPY = 4 +        const val SELECTION_TYPE_NEARBY = 5 +        const val SELECTION_TYPE_EDIT = 6 +        const val SELECTION_TYPE_MODIFY_SHARE = 7 +        const val SELECTION_TYPE_CUSTOM_ACTION = 8 +    } + +    fun logChooserActivityShown(isWorkProfile: Boolean, targetMimeType: String?, systemCost: Long) + +    fun logShareStarted( +        packageName: String?, +        mimeType: String?, +        appProvidedDirect: Int, +        appProvidedApp: Int, +        isWorkprofile: Boolean, +        previewType: Int, +        intent: String?, +        customActionCount: Int, +        modifyShareActionProvided: Boolean +    ) + +    fun logCustomActionSelected(positionPicked: Int) +    fun logShareTargetSelected( +        targetType: Int, +        packageName: String?, +        positionPicked: Int, +        directTargetAlsoRanked: Int, +        numCallerProvided: Int, +        directTargetHashed: HashedStringCache.HashResult?, +        isPinned: Boolean, +        successfullySelected: Boolean, +        selectionCost: Long +    ) + +    fun logDirectShareTargetReceived(category: Int, latency: Int) +    fun logActionShareWithPreview(previewType: Int) +    fun logActionSelected(targetType: Int) +    fun logContentPreviewWarning(uri: Uri?) +    fun logSharesheetTriggered() +    fun logSharesheetAppLoadComplete() +    fun logSharesheetDirectLoadComplete() +    fun logSharesheetDirectLoadTimeout() +    fun logSharesheetProfileChanged() +    fun logSharesheetExpansionChanged(isCollapsed: Boolean) +    fun logSharesheetAppShareRankingTimeout() +    fun logSharesheetEmptyDirectShareRow() +} diff --git a/java/src/com/android/intentresolver/logging/EventLog.java b/java/src/com/android/intentresolver/logging/EventLogImpl.java index b30e825b..84029e76 100644 --- a/java/src/com/android/intentresolver/logging/EventLog.java +++ b/java/src/com/android/intentresolver/logging/EventLogImpl.java @@ -16,7 +16,6 @@  package com.android.intentresolver.logging; -import android.annotation.Nullable;  import android.content.Intent;  import android.metrics.LogMaker;  import android.net.Uri; @@ -24,6 +23,8 @@ import android.provider.MediaStore;  import android.util.HashedStringCache;  import android.util.Log; +import androidx.annotation.Nullable; +  import com.android.intentresolver.ChooserActivity;  import com.android.intentresolver.contentpreview.ContentPreviewType;  import com.android.internal.annotations.VisibleForTesting; @@ -32,84 +33,42 @@ import com.android.internal.logging.InstanceIdSequence;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.UiEvent;  import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLoggerImpl;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;  import com.android.internal.util.FrameworkStatsLog; +import javax.inject.Inject; +  /**   * Helper for writing Sharesheet atoms to statsd log. - * @hide   */ -public class EventLog { +public class EventLogImpl implements EventLog {      private static final String TAG = "ChooserActivity";      private static final boolean DEBUG = true; -    public static final int SELECTION_TYPE_SERVICE = 1; -    public static final int SELECTION_TYPE_APP = 2; -    public static final int SELECTION_TYPE_STANDARD = 3; -    public static final int SELECTION_TYPE_COPY = 4; -    public static final int SELECTION_TYPE_NEARBY = 5; -    public static final int SELECTION_TYPE_EDIT = 6; -    public static final int SELECTION_TYPE_MODIFY_SHARE = 7; -    public static final int SELECTION_TYPE_CUSTOM_ACTION = 8; - -    /** -     * This shim is provided only for testing. In production, clients will only ever use a -     * {@link DefaultFrameworkStatsLogger}. -     */ -    @VisibleForTesting -    interface FrameworkStatsLogger { -        /** Overload to use for logging {@code FrameworkStatsLog.SHARESHEET_STARTED}. */ -        void write( -                int frameworkEventId, -                int appEventId, -                String packageName, -                int instanceId, -                String mimeType, -                int numAppProvidedDirectTargets, -                int numAppProvidedAppTargets, -                boolean isWorkProfile, -                int previewType, -                int intentType, -                int numCustomActions, -                boolean modifyShareActionProvided); - -        /** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */ -        void write( -                int frameworkEventId, -                int appEventId, -                String packageName, -                int instanceId, -                int positionPicked, -                boolean isPinned); -    } -      private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13); -    // A small per-notification ID, used for statsd logging. -    // TODO: consider precomputing and storing as final. -    private static InstanceIdSequence sInstanceIdSequence; -    private InstanceId mInstanceId; +    private final InstanceId mInstanceId;      private final UiEventLogger mUiEventLogger;      private final FrameworkStatsLogger mFrameworkStatsLogger;      private final MetricsLogger mMetricsLogger; -    public EventLog() { -        this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger()); +    public static InstanceIdSequence newIdSequence() { +        return new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);      } -    @VisibleForTesting -    EventLog( -            UiEventLogger uiEventLogger, -            FrameworkStatsLogger frameworkLogger, -            MetricsLogger metricsLogger) { +    @Inject +    public EventLogImpl(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger, +                        MetricsLogger metricsLogger, InstanceId instanceId) {          mUiEventLogger = uiEventLogger;          mFrameworkStatsLogger = frameworkLogger;          mMetricsLogger = metricsLogger; +        mInstanceId = instanceId;      } +      /** Records metrics for the start time of the {@link ChooserActivity}. */ +    @Override      public void logChooserActivityShown(              boolean isWorkProfile, String targetMimeType, long systemCost) {          mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN) @@ -120,6 +79,7 @@ public class EventLog {      }      /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */ +    @Override      public void logShareStarted(              String packageName,              String mimeType, @@ -133,7 +93,7 @@ public class EventLog {          mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,                  /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),                  /* package_name = 2 */ packageName, -                /* instance_id = 3 */ getInstanceId().getId(), +                /* instance_id = 3 */ mInstanceId.getId(),                  /* mime_type = 4 */ mimeType,                  /* num_app_provided_direct_targets = 5 */ appProvidedDirect,                  /* num_app_provided_app_targets = 6 */ appProvidedApp, @@ -149,12 +109,13 @@ public class EventLog {       *       * @param positionPicked index of the custom action within the list of custom actions.       */ +    @Override      public void logCustomActionSelected(int positionPicked) {          mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,                  /* event_id = 1 */                  SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId(),                  /* package_name = 2 */ null, -                /* instance_id = 3 */ getInstanceId().getId(), +                /* instance_id = 3 */ mInstanceId.getId(),                  /* position_picked = 4 */ positionPicked,                  /* is_pinned = 5 */ false);      } @@ -164,6 +125,7 @@ public class EventLog {       * TODO: document parameters and/or consider breaking up by targetType so we don't have to       * support an overly-generic signature.       */ +    @Override      public void logShareTargetSelected(              int targetType,              String packageName, @@ -177,7 +139,7 @@ public class EventLog {          mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,                  /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),                  /* package_name = 2 */ packageName, -                /* instance_id = 3 */ getInstanceId().getId(), +                /* instance_id = 3 */ mInstanceId.getId(),                  /* position_picked = 4 */ positionPicked,                  /* is_pinned = 5 */ isPinned); @@ -209,6 +171,7 @@ public class EventLog {      }      /** Log when direct share targets were received. */ +    @Override      public void logDirectShareTargetReceived(int category, int latency) {          mMetricsLogger.write(new LogMaker(category).setSubtype(latency));      } @@ -217,12 +180,14 @@ public class EventLog {       * Log when we display a preview UI of the specified {@code previewType} as part of our       * Sharesheet session.       */ +    @Override      public void logActionShareWithPreview(int previewType) {          mMetricsLogger.write(                  new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW).setSubtype(previewType));      }      /** Log when the user selects an action button with the specified {@code targetType}. */ +    @Override      public void logActionSelected(int targetType) {          if (targetType == SELECTION_TYPE_COPY) {              LogMaker targetLogMaker = new LogMaker( @@ -232,12 +197,13 @@ public class EventLog {          mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,                  /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),                  /* package_name = 2 */ "", -                /* instance_id = 3 */ getInstanceId().getId(), +                /* instance_id = 3 */ mInstanceId.getId(),                  /* position_picked = 4 */ -1,                  /* is_pinned = 5 */ false);      }      /** Log a warning that we couldn't display the content preview from the supplied {@code uri}. */ +    @Override      public void logContentPreviewWarning(Uri uri) {          // The ContentResolver already logs the exception. Log something more informative.          Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If " @@ -248,55 +214,63 @@ public class EventLog {      }      /** Logs a UiEventReported event for the system sharesheet being triggered by the user. */ +    @Override      public void logSharesheetTriggered() { -        log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, mInstanceId);      }      /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */ +    @Override      public void logSharesheetAppLoadComplete() { -        log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, mInstanceId);      }      /**       * Logs a UiEventReported event for the system sharesheet completing loading service targets.       */ +    @Override      public void logSharesheetDirectLoadComplete() { -        log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, mInstanceId);      }      /**       * Logs a UiEventReported event for the system sharesheet timing out loading service targets.       */ +    @Override      public void logSharesheetDirectLoadTimeout() { -        log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, mInstanceId);      }      /**       * Logs a UiEventReported event for the system sharesheet switching       * between work and main profile.       */ +    @Override      public void logSharesheetProfileChanged() { -        log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, mInstanceId);      }      /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */ +    @Override      public void logSharesheetExpansionChanged(boolean isCollapsed) {          log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED : -                SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId()); +                SharesheetStandardEvent.SHARESHEET_EXPANDED, mInstanceId);      }      /**       * Logs a UiEventReported event for the system sharesheet app share ranking timing out.       */ +    @Override      public void logSharesheetAppShareRankingTimeout() { -        log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, mInstanceId);      }      /**       * Logs a UiEventReported event for the system sharesheet when direct share row is empty.       */ +    @Override      public void logSharesheetEmptyDirectShareRow() { -        log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId()); +        log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, mInstanceId);      }      /** @@ -313,19 +287,6 @@ public class EventLog {      }      /** -     * @return A unique {@link InstanceId} to join across events recorded by this logger instance. -     */ -    private InstanceId getInstanceId() { -        if (mInstanceId == null) { -            if (sInstanceIdSequence == null) { -                sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX); -            } -            mInstanceId = sInstanceIdSequence.newInstanceId(); -        } -        return mInstanceId; -    } - -    /**       * The UiEvent enums that this class can log.       */      enum SharesheetStartedEvent implements UiEventLogger.UiEventEnum { @@ -488,52 +449,4 @@ public class EventLog {                  return 0;          }      } - -    private static class DefaultFrameworkStatsLogger implements FrameworkStatsLogger { -        @Override -        public void write( -                int frameworkEventId, -                int appEventId, -                String packageName, -                int instanceId, -                String mimeType, -                int numAppProvidedDirectTargets, -                int numAppProvidedAppTargets, -                boolean isWorkProfile, -                int previewType, -                int intentType, -                int numCustomActions, -                boolean modifyShareActionProvided) { -            FrameworkStatsLog.write( -                    frameworkEventId, -                    /* event_id = 1 */ appEventId, -                    /* package_name = 2 */ packageName, -                    /* instance_id = 3 */ instanceId, -                    /* mime_type = 4 */ mimeType, -                    /* num_app_provided_direct_targets */ numAppProvidedDirectTargets, -                    /* num_app_provided_app_targets */ numAppProvidedAppTargets, -                    /* is_workprofile */ isWorkProfile, -                    /* previewType = 8 */ previewType, -                    /* intentType = 9 */ intentType, -                    /* num_provided_custom_actions = 10 */ numCustomActions, -                    /* modify_share_action_provided = 11 */ modifyShareActionProvided); -        } - -        @Override -        public void write( -                int frameworkEventId, -                int appEventId, -                String packageName, -                int instanceId, -                int positionPicked, -                boolean isPinned) { -            FrameworkStatsLog.write( -                    frameworkEventId, -                    /* event_id = 1 */ appEventId, -                    /* package_name = 2 */ packageName, -                    /* instance_id = 3 */ instanceId, -                    /* position_picked = 4 */ positionPicked, -                    /* is_pinned = 5 */ isPinned); -        } -    }  } diff --git a/java/src/com/android/intentresolver/logging/EventLogModule.kt b/java/src/com/android/intentresolver/logging/EventLogModule.kt new file mode 100644 index 00000000..eba8ecc8 --- /dev/null +++ b/java/src/com/android/intentresolver/logging/EventLogModule.kt @@ -0,0 +1,46 @@ +/* + * 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.logging + +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLoggerImpl +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +interface EventLogModule { + +    @Binds @ActivityScoped fun eventLog(value: EventLogImpl): EventLog + +    companion object { +        @Provides +        fun instanceId(sequence: InstanceIdSequence): InstanceId = sequence.newInstanceId() + +        @Provides fun uiEventLogger(): UiEventLogger = UiEventLoggerImpl() + +        @Provides fun frameworkLogger(): FrameworkStatsLogger = object : FrameworkStatsLogger {} + +        @Provides fun metricsLogger(): MetricsLogger = MetricsLogger() +    } +} diff --git a/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt new file mode 100644 index 00000000..6508d305 --- /dev/null +++ b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt @@ -0,0 +1,75 @@ +/* + * 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.logging + +import com.android.internal.util.FrameworkStatsLog + +/** A documenting annotation for FrameworkStatsLog methods and their associated UiEvents. */ +internal annotation class ForUiEvent(vararg val uiEventId: Int) + +/** Isolates the specific method signatures to use for each of the logged UiEvents. */ +interface FrameworkStatsLogger { + +    @ForUiEvent(FrameworkStatsLog.SHARESHEET_STARTED) +    fun write( +        frameworkEventId: Int, +        appEventId: Int, +        packageName: String?, +        instanceId: Int, +        mimeType: String?, +        numAppProvidedDirectTargets: Int, +        numAppProvidedAppTargets: Int, +        isWorkProfile: Boolean, +        previewType: Int, +        intentType: Int, +        numCustomActions: Int, +        modifyShareActionProvided: Boolean, +    ) { +        FrameworkStatsLog.write( +            frameworkEventId, /* event_id = 1 */ +            appEventId, /* package_name = 2 */ +            packageName, /* instance_id = 3 */ +            instanceId, /* mime_type = 4 */ +            mimeType, /* num_app_provided_direct_targets */ +            numAppProvidedDirectTargets, /* num_app_provided_app_targets */ +            numAppProvidedAppTargets, /* is_workprofile */ +            isWorkProfile, /* previewType = 8 */ +            previewType, /* intentType = 9 */ +            intentType, /* num_provided_custom_actions = 10 */ +            numCustomActions, /* modify_share_action_provided = 11 */ +            modifyShareActionProvided +        ) +    } + +    @ForUiEvent(FrameworkStatsLog.RANKING_SELECTED) +    fun write( +        frameworkEventId: Int, +        appEventId: Int, +        packageName: String?, +        instanceId: Int, +        positionPicked: Int, +        isPinned: Boolean, +    ) { +        FrameworkStatsLog.write( +            frameworkEventId, /* event_id = 1 */ +            appEventId, /* package_name = 2 */ +            packageName, /* instance_id = 3 */ +            instanceId, /* position_picked = 4 */ +            positionPicked, /* is_pinned = 5 */ +            isPinned +        ) +    } +} diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java index ff2d6a0f..724fa849 100644 --- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java @@ -16,7 +16,6 @@  package com.android.intentresolver.model; -import android.annotation.Nullable;  import android.app.usage.UsageStatsManager;  import android.content.ComponentName;  import android.content.Context; @@ -30,10 +29,13 @@ import android.os.Message;  import android.os.UserHandle;  import android.util.Log; -import com.android.intentresolver.logging.EventLog; +import androidx.annotation.Nullable; +  import com.android.intentresolver.ResolvedComponentInfo;  import com.android.intentresolver.ResolverActivity; +import com.android.intentresolver.ResolverListController;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.logging.EventLog;  import java.text.Collator;  import java.util.ArrayList; @@ -75,6 +77,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC      private EventLog mEventLog;      protected final Handler mHandler = new Handler(Looper.getMainLooper()) { +        @Override          public void handleMessage(Message msg) {              switch (msg.what) {                  case RANKER_SERVICE_RESULT: @@ -229,7 +232,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC       * {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link       * #compare(ResolvedComponentInfo, ResolvedComponentInfo)}       */ -    abstract int compare(ResolveInfo lhs, ResolveInfo rhs); +    public abstract int compare(ResolveInfo lhs, ResolveInfo rhs);      /**       * Computes features for each target. This will be called before calls to {@link @@ -245,7 +248,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC      }      /** Implementation of compute called after {@link #beforeCompute()}. */ -    abstract void doCompute(List<ResolvedComponentInfo> targets); +    public abstract void doCompute(List<ResolvedComponentInfo> targets);      /**       * Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo} @@ -254,12 +257,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC      public abstract float getScore(TargetInfo targetInfo);      /** Handles result message sent to mHandler. */ -    abstract void handleResultMessage(Message message); +    public abstract void handleResultMessage(Message message);      /**       * Reports to UsageStats what was chosen.       */ -    public final void updateChooserCounts(String packageName, UserHandle user, String action) { +    public void updateChooserCounts(String packageName, UserHandle user, String action) {          if (mUsmMap.containsKey(user)) {              mUsmMap.get(user).reportChooserSelection(                      packageName, diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java index 621ae306..0651d26c 100644 --- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java @@ -18,7 +18,6 @@ package com.android.intentresolver.model;  import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH; -import android.annotation.Nullable;  import android.app.prediction.AppPredictor;  import android.app.prediction.AppTarget;  import android.app.prediction.AppTargetEvent; @@ -31,9 +30,12 @@ import android.os.Message;  import android.os.UserHandle;  import android.util.Log; -import com.android.intentresolver.logging.EventLog; +import androidx.annotation.Nullable; +  import com.android.intentresolver.ResolvedComponentInfo;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.shortcuts.ScopedAppTargetListCallback;  import com.google.android.collect.Lists; @@ -85,12 +87,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp      }      @Override -    int compare(ResolveInfo lhs, ResolveInfo rhs) { +    public int compare(ResolveInfo lhs, ResolveInfo rhs) {          return mComparatorModel.getComparator().compare(lhs, rhs);      }      @Override -    void doCompute(List<ResolvedComponentInfo> targets) { +    public void doCompute(List<ResolvedComponentInfo> targets) {          if (targets.isEmpty()) {              mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);              return; @@ -105,33 +107,44 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp                      .setClassName(target.name.getClassName())                      .build());          } -        mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(), -                sortedAppTargets -> { -                    if (sortedAppTargets.isEmpty()) { -                        Log.i(TAG, "AppPredictionService disabled. Using resolver."); -                        // APS for chooser is disabled. Fallback to resolver. -                        mResolverRankerService = -                                new ResolverRankerServiceResolverComparator( -                                        mContext, -                                        mIntent, -                                        mReferrerPackage, -                                        () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), -                                        getEventLog(), -                                        mUser, -                                        mPromoteToFirst); -                        mComparatorModel = buildUpdatedModel(); -                        mResolverRankerService.compute(targets); -                    } else { -                        Log.i(TAG, "AppPredictionService response received"); -                        // Skip sending to Handler which takes extra time to dispatch messages. -                        handleResult(sortedAppTargets); -                    } -                } +        mAppPredictor.sortTargets( +                appTargets, +                Executors.newSingleThreadExecutor(), +                new ScopedAppTargetListCallback( +                        mContext, +                        sortedAppTargets -> { +                            onAppTargetsSorted(targets, sortedAppTargets); +                            return kotlin.Unit.INSTANCE; +                        }).toConsumer()          );      } +    private void onAppTargetsSorted( +            List<ResolvedComponentInfo> targets, List<AppTarget> sortedAppTargets) { +        if (sortedAppTargets.isEmpty()) { +            Log.i(TAG, "AppPredictionService disabled. Using resolver."); +            // APS for chooser is disabled. Fallback to resolver. +            mResolverRankerService = +                    new ResolverRankerServiceResolverComparator( +                            mContext, +                            mIntent, +                            mReferrerPackage, +                            () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), +                            getEventLog(), +                            mUser, +                            mPromoteToFirst); +            mComparatorModel = buildUpdatedModel(); +            mResolverRankerService.compute(targets); +        } else { +            Log.i(TAG, "AppPredictionService response received"); +            // Skip sending to Handler which takes extra time to dispatch +            // messages. +            handleResult(sortedAppTargets); +        } +    } +      @Override -    void handleResultMessage(Message msg) { +    public void handleResultMessage(Message msg) {          // Null value is okay if we have defaulted to the ResolverRankerService.          if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {              final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj; diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java index 7d473660..f3804154 100644 --- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java @@ -17,7 +17,6 @@  package com.android.intentresolver.model; -import android.annotation.Nullable;  import android.app.usage.UsageStats;  import android.content.ComponentName;  import android.content.Context; @@ -39,9 +38,11 @@ import android.service.resolver.ResolverRankerService;  import android.service.resolver.ResolverTarget;  import android.util.Log; -import com.android.intentresolver.logging.EventLog; +import androidx.annotation.Nullable; +  import com.android.intentresolver.ResolvedComponentInfo;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.logging.EventLog;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -101,9 +102,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom       *                        the userSpace provided by context.       */      public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, -            String referrerPackage, Runnable afterCompute, -            EventLog eventLog, UserHandle targetUserSpace, -            ComponentName promoteToFirst) { +                                                   String referrerPackage, Runnable afterCompute, +                                                   EventLog eventLog, UserHandle targetUserSpace, +                                                   ComponentName promoteToFirst) {          this(launchedFromContext, intent, referrerPackage, afterCompute, eventLog,                  Lists.newArrayList(targetUserSpace), promoteToFirst);      } @@ -117,9 +118,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom       *                            different from the userSpace provided by context.       */      public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, -            String referrerPackage, Runnable afterCompute, -            EventLog eventLog, List<UserHandle> targetUserSpaceList, -            @Nullable ComponentName promoteToFirst) { +            String referrerPackage, Runnable afterCompute, EventLog eventLog, +            List<UserHandle> targetUserSpaceList, @Nullable ComponentName promoteToFirst) {          super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst);          mCollator = Collator.getInstance(                  launchedFromContext.getResources().getConfiguration().locale); diff --git a/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt new file mode 100644 index 00000000..9606a6a1 --- /dev/null +++ b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt @@ -0,0 +1,58 @@ +/* + * 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.shortcuts + +import android.app.prediction.AppPredictor +import android.app.prediction.AppTarget +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch + +/** + * A memory leak workaround for b/290971946. Drops the references to the actual [callback] when the + * [scope] is cancelled allowing it to be garbage-collected (and only leaking this instance). + */ +class ScopedAppTargetListCallback( +    scope: CoroutineScope?, +    callback: (List<AppTarget>) -> Unit, +) { + +    @Volatile private var callbackRef: ((List<AppTarget>) -> Unit)? = callback + +    constructor( +        context: Context, +        callback: (List<AppTarget>) -> Unit, +    ) : this((context as? LifecycleOwner)?.lifecycle?.coroutineScope, callback) + +    init { +        scope?.launch { awaitCancellation() }?.invokeOnCompletion { callbackRef = null } +    } + +    private fun notifyCallback(result: List<AppTarget>) { +        callbackRef?.invoke(result) +    } + +    fun toConsumer(): Consumer<MutableList<AppTarget>?> = +        Consumer<MutableList<AppTarget>?> { notifyCallback(it ?: emptyList()) } + +    fun toAppPredictorCallback(): AppPredictor.Callback = +        AppPredictor.Callback { notifyCallback(it) } +} diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt index f05542e2..a8b59fb0 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -35,14 +35,13 @@ import androidx.annotation.MainThread  import androidx.annotation.OpenForTesting  import androidx.annotation.VisibleForTesting  import androidx.annotation.WorkerThread -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope  import com.android.intentresolver.chooser.DisplayResolveInfo  import com.android.intentresolver.measurements.Tracer  import com.android.intentresolver.measurements.runTracing  import java.util.concurrent.Executor  import java.util.function.Consumer  import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope  import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.asExecutor  import kotlinx.coroutines.channels.BufferOverflow @@ -50,6 +49,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow  import kotlinx.coroutines.flow.combine  import kotlinx.coroutines.flow.filter  import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.isActive  import kotlinx.coroutines.launch  /** @@ -58,14 +58,14 @@ import kotlinx.coroutines.launch   * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut   * updates. The shortcut loading is triggered in the constructor or by the [reset] method, the   * processing happens on the [dispatcher] and the result is delivered through the [callback] on the - * default [lifecycle]'s dispatcher, the main thread. + * default [scope]'s dispatcher, the main thread.   */  @OpenForTesting  open class ShortcutLoader  @VisibleForTesting  constructor(      private val context: Context, -    private val lifecycle: Lifecycle, +    private val scope: CoroutineScope,      private val appPredictor: AppPredictorProxy?,      private val userHandle: UserHandle,      private val isPersonalProfile: Boolean, @@ -75,7 +75,9 @@ constructor(  ) {      private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter()      private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager -    private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) } +    private val appPredictorCallback = +        ScopedAppTargetListCallback(scope) { onAppPredictorCallback(it) }.toAppPredictorCallback() +      private val appTargetSource =          MutableSharedFlow<Array<DisplayResolveInfo>?>(              replay = 1, @@ -84,19 +86,19 @@ constructor(      private val shortcutSource =          MutableSharedFlow<ShortcutData?>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)      private val isDestroyed -        get() = !lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) +        get() = !scope.isActive      @MainThread      constructor(          context: Context, -        lifecycle: Lifecycle, +        scope: CoroutineScope,          appPredictor: AppPredictor?,          userHandle: UserHandle,          targetIntentFilter: IntentFilter?,          callback: Consumer<Result>      ) : this(          context, -        lifecycle, +        scope,          appPredictor?.let { AppPredictorProxy(it) },          userHandle,          userHandle == UserHandle.of(ActivityManager.getCurrentUser()), @@ -107,7 +109,7 @@ constructor(      init {          appPredictor?.registerPredictionUpdates(dispatcher.asExecutor(), appPredictorCallback) -        lifecycle.coroutineScope +        scope              .launch {                  appTargetSource                      .combine(shortcutSource) { appTargets, shortcutData -> @@ -135,13 +137,13 @@ constructor(          reset()      } -    /** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */ +    /** Clear application targets (see [updateAppTargets] and initiate shortcuts loading. */      @OpenForTesting      open fun reset() {          Log.d(TAG, "reset shortcut loader for user $userHandle")          appTargetSource.tryEmit(null)          shortcutSource.tryEmit(null) -        lifecycle.coroutineScope.launch(dispatcher) { loadShortcuts() } +        scope.launch(dispatcher) { loadShortcuts() }      }      /** diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java index a37d6558..31929948 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java @@ -16,8 +16,6 @@  package com.android.intentresolver.shortcuts; -import android.annotation.NonNull; -import android.annotation.Nullable;  import android.app.prediction.AppTarget;  import android.content.Intent;  import android.content.pm.ShortcutInfo; @@ -25,6 +23,9 @@ import android.content.pm.ShortcutManager;  import android.os.Bundle;  import android.service.chooser.ChooserTarget; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +  import java.util.ArrayList;  import java.util.Collections;  import java.util.Comparator; diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt new file mode 100644 index 00000000..c81bed09 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt @@ -0,0 +1,156 @@ +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 + * test activities) should be in this interface. Expect there to be one implementation for each + * 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() +} + +/** + * Logic that is common to all IntentResolver activities. Anything that is the same across + * activities (including test activities), should live here. + */ +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? +} + +/** + * Concrete implementation of the [CommonActivityLogic] interface meant to be delegated to by + * [ActivityLogic] implementations. Test implementations of [ActivityLogic] may need to create their + * own [CommonActivityLogic] implementation. + */ +class CommonActivityLogicImpl( +    override val tag: String, +    activityProvider: () -> 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 +            } +        } +    } + +    override val userManager: UserManager by lazy { activity.getSystemService()!! } + +    override val devicePolicyManager: DevicePolicyManager by lazy { activity.getSystemService()!! } + +    override val annotatedUserHandles: AnnotatedUserHandles? by lazy { +        try { +            AnnotatedUserHandles.forShareActivity(activity) +        } catch (e: SecurityException) { +            Log.e(tag, "Request from UID without necessary permissions", e) +            null +        } +    } + +    override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy { +        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 new file mode 100644 index 00000000..db840387 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java @@ -0,0 +1,395 @@ +/* + * 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; + +import android.app.Activity; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.service.chooser.ChooserAction; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.intentresolver.R; +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.widget.ActionRow; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +/** + * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application + * requirements of Sharesheet / {@link ChooserActivity}. + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory { +    /** +     * Delegate interface to launch activities when the actions are selected. +     */ +    public interface ActionActivityStarter { +        /** +         * Request an activity launch for the provided target. Implementations may choose to exit +         * the current activity when the target is launched. +         */ +        void safelyStartActivityAsPersonalProfileUser(TargetInfo info); + +        /** +         * Request an activity launch for the provided target, optionally employing the specified +         * shared element transition. Implementations may choose to exit the current activity when +         * the target is launched. +         */ +        default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( +                TargetInfo info, View sharedElement, String sharedElementName) { +            safelyStartActivityAsPersonalProfileUser(info); +        } +    } + +    private static final String TAG = "ChooserActions"; + +    private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION +            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION +            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION +            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; + +    // Boolean extra used to inform the editor that it may want to customize the editing experience +    // for the sharesheet editing flow. +    private static final String EDIT_SOURCE = "edit_source"; +    private static final String EDIT_SOURCE_SHARESHEET = "sharesheet"; + +    private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; +    private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; + +    private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image"; + +    private final Context mContext; + +    @Nullable +    private final Runnable mCopyButtonRunnable; +    private final Runnable mEditButtonRunnable; +    private final ImmutableList<ChooserAction> mCustomActions; +    private final @Nullable ChooserAction mModifyShareAction; +    private final Consumer<Boolean> mExcludeSharedTextAction; +    private final Consumer</* @Nullable */ Integer> mFinishCallback; +    private final EventLog mLog; + +    /** +     * @param context +     * @param imageEditor an explicit Activity to launch for editing images +     * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text" +     * setting is updated. The argument is whether the shared text is to be excluded. +     * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image +     * View in the Sharesheet UI, if any, or null. +     * @param activityStarter a delegate to launch activities when actions are selected. +     * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was +     * completed). +     */ +    public ChooserActionFactory( +            Context context, +            Intent targetIntent, +            String referrerPackageName, +            List<ChooserAction> chooserActions, +            ChooserAction modifyShareAction, +            Optional<ComponentName> imageEditor, +            EventLog log, +            Consumer<Boolean> onUpdateSharedTextIsExcluded, +            Callable</* @Nullable */ View> firstVisibleImageQuery, +            ActionActivityStarter activityStarter, +            Consumer</* @Nullable */ Integer> finishCallback) { +        this( +                context, +                makeCopyButtonRunnable( +                        context, +                        targetIntent, +                        referrerPackageName, +                        finishCallback, +                        log), +                makeEditButtonRunnable( +                        getEditSharingTarget( +                                context, +                                targetIntent, +                                imageEditor), +                        firstVisibleImageQuery, +                        activityStarter, +                        log), +                chooserActions, +                modifyShareAction, +                onUpdateSharedTextIsExcluded, +                log, +                finishCallback); +    } + +    @VisibleForTesting +    ChooserActionFactory( +            Context context, +            @Nullable Runnable copyButtonRunnable, +            Runnable editButtonRunnable, +            List<ChooserAction> customActions, +            @Nullable ChooserAction modifyShareAction, +            Consumer<Boolean> onUpdateSharedTextIsExcluded, +            EventLog log, +            Consumer</* @Nullable */ Integer> finishCallback) { +        mContext = context; +        mCopyButtonRunnable = copyButtonRunnable; +        mEditButtonRunnable = editButtonRunnable; +        mCustomActions = ImmutableList.copyOf(customActions); +        mModifyShareAction = modifyShareAction; +        mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; +        mLog = log; +        mFinishCallback = finishCallback; +    } + +    @Override +    @Nullable +    public Runnable getEditButtonRunnable() { +        return mEditButtonRunnable; +    } + +    @Override +    @Nullable +    public Runnable getCopyButtonRunnable() { +        return mCopyButtonRunnable; +    } + +    /** Create custom actions */ +    @Override +    public List<ActionRow.Action> createCustomActions() { +        List<ActionRow.Action> actions = new ArrayList<>(); +        for (int i = 0; i < mCustomActions.size(); i++) { +            final int position = i; +            ActionRow.Action actionRow = createCustomAction( +                    mContext, +                    mCustomActions.get(i), +                    mFinishCallback, +                    () -> { +                        mLog.logCustomActionSelected(position); +                    } +            ); +            if (actionRow != null) { +                actions.add(actionRow); +            } +        } +        return actions; +    } + +    /** +     * Provides a share modification action, if any. +     */ +    @Override +    @Nullable +    public ActionRow.Action getModifyShareAction() { +        return createCustomAction( +                mContext, +                mModifyShareAction, +                mFinishCallback, +                () -> { +                    mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE); +                }); +    } + +    /** +     * <p> +     * Creates an exclude-text action that can be called when the user changes shared text +     * status in the Media + Text preview. +     * </p> +     * <p> +     * <code>true</code> argument value indicates that the text should be excluded. +     * </p> +     */ +    @Override +    public Consumer<Boolean> getExcludeSharedTextAction() { +        return mExcludeSharedTextAction; +    } + +    @Nullable +    private static Runnable makeCopyButtonRunnable( +            Context context, +            Intent targetIntent, +            String referrerPackageName, +            Consumer<Integer> finishCallback, +            EventLog log) { +        final ClipData clipData; +        try { +            clipData = extractTextToCopy(targetIntent); +        } catch (Throwable t) { +            Log.e(TAG, "Failed to extract data to copy", t); +            return null; +        } +        if (clipData == null) { +            return null; +        } +        return () -> { +            ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService( +                    Context.CLIPBOARD_SERVICE); +            clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName); + +            log.logActionSelected(EventLog.SELECTION_TYPE_COPY); +            finishCallback.accept(Activity.RESULT_OK); +        }; +    } + +    @Nullable +    private static ClipData extractTextToCopy(Intent targetIntent) { +        if (targetIntent == null) { +            return null; +        } + +        final String action = targetIntent.getAction(); + +        ClipData clipData = null; +        if (Intent.ACTION_SEND.equals(action)) { +            String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); + +            if (extraText != null) { +                clipData = ClipData.newPlainText(null, extraText); +            } else { +                Log.w(TAG, "No data available to copy to clipboard"); +            } +        } else { +            // expected to only be visible with ACTION_SEND (when a text is shared) +            Log.d(TAG, "Action (" + action + ") not supported for copying to clipboard"); +        } +        return clipData; +    } + +    private static TargetInfo getEditSharingTarget( +            Context context, +            Intent originalIntent, +            Optional<ComponentName> imageEditor) { + +        final Intent resolveIntent = new Intent(originalIntent); +        // Retain only URI permission grant flags if present. Other flags may prevent the scene +        // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION, +        // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed. +        resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS); +        imageEditor.ifPresent(resolveIntent::setComponent); +        resolveIntent.setAction(Intent.ACTION_EDIT); +        resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET); +        String originalAction = originalIntent.getAction(); +        if (Intent.ACTION_SEND.equals(originalAction)) { +            if (resolveIntent.getData() == null) { +                Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM); +                if (uri != null) { +                    String mimeType = context.getContentResolver().getType(uri); +                    resolveIntent.setDataAndType(uri, mimeType); +                } +            } +        } else { +            Log.e(TAG, originalAction + " is not supported."); +            return null; +        } +        final ResolveInfo ri = context.getPackageManager().resolveActivity( +                resolveIntent, PackageManager.GET_META_DATA); +        if (ri == null || ri.activityInfo == null) { +            Log.e(TAG, "Device-specified editor (" + imageEditor + ") not available"); +            return null; +        } + +        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( +                originalIntent, +                ri, +                context.getString(R.string.screenshot_edit), +                "", +                resolveIntent); +        dri.getDisplayIconHolder().setDisplayIcon( +                context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit)); +        return dri; +    } + +    private static Runnable makeEditButtonRunnable( +            TargetInfo editSharingTarget, +            Callable</* @Nullable */ View> firstVisibleImageQuery, +            ActionActivityStarter activityStarter, +            EventLog log) { +        return () -> { +            // Log share completion via edit. +            log.logActionSelected(EventLog.SELECTION_TYPE_EDIT); + +            View firstImageView = null; +            try { +                firstImageView = firstVisibleImageQuery.call(); +            } catch (Exception e) { /* ignore */ } +            // Action bar is user-independent; always start as primary. +            if (firstImageView == null) { +                activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget); +            } else { +                activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( +                        editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT); +            } +        }; +    } + +    @Nullable +    private static ActionRow.Action createCustomAction( +            Context context, +            ChooserAction action, +            Consumer<Integer> finishCallback, +            Runnable loggingRunnable) { +        if (action == null || action.getAction() == null) { +            return null; +        } +        Drawable icon = action.getIcon().loadDrawable(context); +        if (icon == null && TextUtils.isEmpty(action.getLabel())) { +            return null; +        } +        return new ActionRow.Action( +                action.getLabel(), +                icon, +                () -> { +                    try { +                        action.getAction().send( +                                null, +                                0, +                                null, +                                null, +                                null, +                                null, +                                ActivityOptions.makeCustomAnimation( +                                                context, +                                                R.anim.slide_in_right, +                                                R.anim.slide_out_left) +                                        .toBundle()); +                    } catch (PendingIntent.CanceledException e) { +                        Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled"); +                    } +                    if (loggingRunnable != null) { +                        loggingRunnable.run(); +                    } +                    finishCallback.accept(Activity.RESULT_OK); +                } +        ); +    } +} diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java new file mode 100644 index 00000000..70812642 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -0,0 +1,1845 @@ +/* + * Copyright (C) 2008 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 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.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 androidx.lifecycle.LifecycleKt.getCoroutineScope; + +import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; + +import static java.util.Objects.requireNonNull; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Insets; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.service.chooser.ChooserTarget; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.widget.TextView; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.ViewPager; + +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.R; +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.ResolverViewPager; +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.PreviewViewModel; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.TargetDataLoader; +import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.measurements.Tracer; +import com.android.intentresolver.model.AbstractResolverComparator; +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.emptystate.NoCrossProfileEmptyStateProvider; +import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.v2.platform.ImageEditor; +import com.android.intentresolver.v2.platform.NearbyShare; +import com.android.intentresolver.widget.ImagePreviewView; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.PackageMonitor; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import dagger.hilt.android.AndroidEntryPoint; + +import kotlin.Unit; + +import java.text.Collator; +import java.util.ArrayList; +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.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * The Chooser Activity handles intent resolution specifically for sharing intents - + * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. + * + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@AndroidEntryPoint(ResolverActivity.class) +public class ChooserActivity extends Hilt_ChooserActivity implements +        ResolverListAdapter.ResolverListCommunicator { +    private static final String TAG = "ChooserActivity"; + +    /** +     * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself +     * in onStop when launched in a new task. If this extra is set to true, we do not finish +     * ourselves when onStop gets called. +     */ +    public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP +            = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP"; + +    /** +     * Transition name for the first image preview. +     * To be used for shared element transition into this activity. +     * @hide +     */ +    public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image"; + +    private static final boolean DEBUG = true; + +    public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; +    private static final String SHORTCUT_TARGET = "shortcut_target"; + +    // 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`. +    // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their +    // intermediate data, and then these members can be removed. +    private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>(); +    private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>(); + +    private static final int TARGET_TYPE_DEFAULT = 0; +    private static final int TARGET_TYPE_CHOOSER_TARGET = 1; +    private static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; +    private static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3; + +    private static final int SCROLL_STATUS_IDLE = 0; +    private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1; +    private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; + +    @Inject public FeatureFlags mFeatureFlags; +    @Inject public EventLog mEventLog; +    @Inject @ImageEditor public Optional<ComponentName> mImageEditor; +    @Inject @NearbyShare public Optional<ComponentName> mNearbyShare; +    @Inject public TargetDataLoader mTargetDataLoader; + +    private ChooserRefinementManager mRefinementManager; + +    private ChooserContentPreviewUi mChooserContentPreviewUi; + +    private boolean mShouldDisplayLandscape; +    private long mChooserShownTime; +    protected boolean mIsSuccessfullySelected; + +    private int mCurrAvailableWidth = 0; +    private Insets mLastAppliedInsets = null; +    private int mLastNumberOfChildren = -1; +    private int mMaxTargetsPerRow = 1; + +    private static final int MAX_LOG_RANK_POSITION = 12; + +    // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters. +    private static final int MAX_EXTRA_INITIAL_INTENTS = 2; +    private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; + +    private SharedPreferences mPinnedSharedPrefs; +    private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; + +    private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5); + +    private int mScrollStatus = SCROLL_STATUS_IDLE; + +    @VisibleForTesting +    protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter; +    private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = +            new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); + +    private View mContentView = null; + +    private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>(); + +    private boolean mExcludeSharedText = false; +    /** +     * When we intend to finish the activity with a shared element transition, we can't immediately +     * finish() when the transition is invoked, as the receiving end may not be able to start the +     * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop +     * in order to wait for the transition to begin. +     */ +    private boolean mFinishWhenStopped = false; + +    private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); + +    @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); +    } + +    private void init() { +        if (getChooserRequest() == null) { +            finish(); +            return; +        } +        if (isFinishing()) { +            // Performing a clean exit: +            //    Skip initializing any additional resources. +            return; +        } +        setTheme(mLogic.getThemeResId()); + +        getEventLog().logSharesheetTriggered(); + +        mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); + +        mRefinementManager.getRefinementCompletion().observe(this, completion -> { +            if (completion.consume()) { +                TargetInfo targetInfo = completion.getTargetInfo(); +                // targetInfo is non-null if the refinement process was successful. +                if (targetInfo != null) { +                    maybeRemoveSharedText(targetInfo); + +                    // We already block suspended targets from going to refinement, and we probably +                    // 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); +                } + +                finish(); +            } +        }); + +        BasePreviewViewModel previewViewModel = +                new ViewModelProvider(this, createPreviewViewModelFactory()) +                        .get(BasePreviewViewModel.class); +        ChooserRequestParameters chooserRequest = requireChooserRequest(); +        mChooserContentPreviewUi = new ChooserContentPreviewUi( +                getCoroutineScope(getLifecycle()), +                previewViewModel.createOrReuseProvider(chooserRequest.getTargetIntent()), +                chooserRequest.getTargetIntent(), +                previewViewModel.createOrReuseImageLoader(), +                createChooserActionFactory(), +                mEnterTransitionAnimationDelegate, +                new HeadlineGeneratorImpl(this)); + +        updateStickyContentPreview(); +        if (shouldShowStickyContentPreview() +                || mChooserMultiProfilePagerAdapter +                .getCurrentRootAdapter().getSystemRowCount() != 0) { +            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); + +            mResolverDrawerLayout.setOnCollapsedChangedListener( +                    isCollapsed -> { +                        mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed); +                        getEventLog().logSharesheetExpansionChanged(isCollapsed); +                    }); +        } + +        if (DEBUG) { +            Log.d(TAG, "System Time Cost is " + systemCost); +        } + +        getEventLog().logShareStarted( +                mLogic.getReferrerPackageName(), +                chooserRequest.getTargetType(), +                chooserRequest.getCallerChooserTargets().size(), +                (chooserRequest.getInitialIntents() == null) +                        ? 0 : chooserRequest.getInitialIntents().length, +                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); + +        mPinnedSharedPrefs = getPinnedSharedPrefs(this); +        mMaxTargetsPerRow = +                getResources().getInteger(R.integer.config_chooser_max_targets_per_row); +        mShouldDisplayLandscape = +                shouldDisplayLandscape(getResources().getConfiguration().orientation); + + +        ChooserRequestParameters chooserRequest = getChooserRequest(); +        if (chooserRequest == null) { +            return Unit.INSTANCE; +        } +        setRetainInOnStop(chooserRequest.shouldRetainInOnStop()); + +        createProfileRecords( +                new AppPredictorFactory( +                        this, +                        chooserRequest.getSharedText(), +                        chooserRequest.getTargetIntentFilter() +                ), +                chooserRequest.getTargetIntentFilter() +        ); +        return Unit.INSTANCE; +    } + +    @Nullable +    private ChooserRequestParameters getChooserRequest() { +        return ((ChooserActivityLogic) mLogic).getChooserRequestParameters(); +    } + +    private ChooserRequestParameters requireChooserRequest() { +        return requireNonNull(getChooserRequest()); +    } + +    private AnnotatedUserHandles requireAnnotatedUserHandles() { +        return requireNonNull(mLogic.getAnnotatedUserHandles()); +    } + +    private void createProfileRecords( +            AppPredictorFactory factory, IntentFilter targetIntentFilter) { +        UserHandle mainUserHandle = requireAnnotatedUserHandles().personalProfileUserHandle; +        ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); +        if (record.shortcutLoader == null) { +            Tracer.INSTANCE.endLaunchToShortcutTrace(); +        } + +        UserHandle workUserHandle = requireAnnotatedUserHandles().workProfileUserHandle; +        if (workUserHandle != null) { +            createProfileRecord(workUserHandle, targetIntentFilter, factory); +        } +    } + +    private ProfileRecord createProfileRecord( +            UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { +        AppPredictor appPredictor = factory.create(userHandle); +        ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() +                    ? null +                    : createShortcutLoader( +                            this, +                            appPredictor, +                            userHandle, +                            targetIntentFilter, +                            shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); +        ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader); +        mProfileRecords.put(userHandle.getIdentifier(), record); +        return record; +    } + +    @Nullable +    private ProfileRecord getProfileRecord(UserHandle userHandle) { +        return mProfileRecords.get(userHandle.getIdentifier(), null); +    } + +    @VisibleForTesting +    protected ShortcutLoader createShortcutLoader( +            Context context, +            AppPredictor appPredictor, +            UserHandle userHandle, +            IntentFilter targetIntentFilter, +            Consumer<ShortcutLoader.Result> callback) { +        return new ShortcutLoader( +                context, +                getCoroutineScope(getLifecycle()), +                appPredictor, +                userHandle, +                targetIntentFilter, +                callback); +    } + +    static SharedPreferences getPinnedSharedPrefs(Context context) { +        return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); +    } + +    @Override +    protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            TargetDataLoader targetDataLoader) { +        if (shouldShowTabs()) { +            mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( +                    initialIntents, rList, filterLastUsed, targetDataLoader); +        } else { +            mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( +                    initialIntents, rList, filterLastUsed, targetDataLoader); +        } +        return mChooserMultiProfilePagerAdapter; +    } + +    @Override +    protected EmptyStateProvider createBlockerEmptyStateProvider() { +        final boolean isSendAction = requireChooserRequest().isSendActionTarget(); + +        final EmptyState noWorkToPersonalEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this, +                        /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, +                        /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, +                        /* devicePolicyStringSubtitleId= */ +                        isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL, +                        /* defaultSubtitleResource= */ +                        isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation +                                : R.string.resolver_cant_access_personal_apps_explanation, +                        /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, +                        /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); + +        final EmptyState noPersonalToWorkEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this, +                        /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, +                        /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, +                        /* devicePolicyStringSubtitleId= */ +                        isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK, +                        /* defaultSubtitleResource= */ +                        isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation +                                : R.string.resolver_cant_access_work_apps_explanation, +                        /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, +                        /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER); + +        return new NoCrossProfileEmptyStateProvider( +                requireAnnotatedUserHandles().personalProfileUserHandle, +                noWorkToPersonalEmptyState, +                noPersonalToWorkEmptyState, +                createCrossProfileIntentsChecker(), +                requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch); +    } + +    private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            TargetDataLoader targetDataLoader) { +        ChooserGridAdapter adapter = createChooserGridAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                initialIntents, +                rList, +                filterLastUsed, +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, +                targetDataLoader); +        return new ChooserMultiProfilePagerAdapter( +                /* context */ this, +                adapter, +                createEmptyStateProvider(/* workProfileUserHandle= */ null), +                /* workProfileQuietModeChecker= */ () -> false, +                /* workProfileUserHandle= */ null, +                requireAnnotatedUserHandles().cloneProfileUserHandle, +                mMaxTargetsPerRow, +                mFeatureFlags); +    } + +    private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            TargetDataLoader targetDataLoader) { +        int selectedProfile = findSelectedProfile(); +        ChooserGridAdapter personalAdapter = createChooserGridAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                selectedProfile == PROFILE_PERSONAL ? initialIntents : null, +                rList, +                filterLastUsed, +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, +                targetDataLoader); +        ChooserGridAdapter workAdapter = createChooserGridAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                selectedProfile == PROFILE_WORK ? initialIntents : null, +                rList, +                filterLastUsed, +                /* userHandle */ requireAnnotatedUserHandles().workProfileUserHandle, +                targetDataLoader); +        return new ChooserMultiProfilePagerAdapter( +                /* context */ this, +                personalAdapter, +                workAdapter, +                createEmptyStateProvider(requireAnnotatedUserHandles().workProfileUserHandle), +                () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(), +                selectedProfile, +                requireAnnotatedUserHandles().workProfileUserHandle, +                requireAnnotatedUserHandles().cloneProfileUserHandle, +                mMaxTargetsPerRow, +                mFeatureFlags); +    } + +    private int findSelectedProfile() { +        int selectedProfile = getSelectedProfileExtra(); +        if (selectedProfile == -1) { +            selectedProfile = getProfileForUser( +                    requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch); +        } +        return selectedProfile; +    } + +    /** +     * Check if the profile currently used is a work profile. +     * @return true if it is work profile, false if it is parent profile (or no work profile is +     * set up) +     */ +    protected boolean isWorkProfile() { +        return getSystemService(UserManager.class) +                .getUserInfo(UserHandle.myUserId()).isManagedProfile(); +    } + +    @Override +    protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { +        return new PackageMonitor() { +            @Override +            public void onSomePackagesChanged() { +                handlePackagesChanged(listAdapter); +            } +        }; +    } + +    /** +     * Update UI to reflect changes in data. +     */ +    public void handlePackagesChanged() { +        handlePackagesChanged(/* listAdapter */ null); +    } + +    /** +     * Update UI to reflect changes in data. +     * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if +     * available. +     */ +    private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) { +        // Refresh pinned items +        mPinnedSharedPrefs = getPinnedSharedPrefs(this); +        if (listAdapter == null) { +            mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs(); +        } 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); +        ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); +        if (viewPager.isLayoutRtl()) { +            mMultiProfilePagerAdapter.setupViewPager(viewPager); +        } + +        mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); +        mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row); +        mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow); +        adjustPreviewWidth(newConfig.orientation, null); +        updateStickyContentPreview(); +        updateTabPadding(); +    } + +    private boolean shouldDisplayLandscape(int orientation) { +        // Sharesheet fixes the # of items per row and therefore can not correctly lay out +        // when in the restricted size of multi-window mode. In the future, would be nice +        // to use minimum dp size requirements instead +        return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode(); +    } + +    private void adjustPreviewWidth(int orientation, View parent) { +        int width = -1; +        if (mShouldDisplayLandscape) { +            width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); +        } + +        parent = parent == null ? getWindow().getDecorView() : parent; + +        updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent); +    } + +    private void updateTabPadding() { +        if (shouldShowTabs()) { +            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 +            // paddingHorizontal. +            float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize) +                    / mMaxTargetsPerRow / 2; +            // Subtract the margin the buttons already have. +            padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin); +            tabs.setPadding((int) padding, 0, (int) padding, 0); +        } +    } + +    private void updateLayoutWidth(int layoutResourceId, int width, View parent) { +        View view = parent.findViewById(layoutResourceId); +        if (view != null && view.getLayoutParams() != null) { +            LayoutParams params = view.getLayoutParams(); +            params.width = width; +            view.setLayoutParams(params); +        } +    } + +    /** +     * Create a view that will be shown in the content preview area +     * @param parent reference to the parent container where the view should be attached to +     * @return content preview view +     */ +    protected ViewGroup createContentPreviewView(ViewGroup parent) { +        ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( +                getResources(), +                getLayoutInflater(), +                parent, +                mFeatureFlags.scrollablePreview() +                        ? findViewById(R.id.chooser_headline_row_container) +                        : null); + +        if (layout != null) { +            adjustPreviewWidth(getResources().getConfiguration().orientation, layout); +        } + +        return layout; +    } + +    @Nullable +    private View getFirstVisibleImgPreviewView() { +        View imagePreview = findViewById(R.id.scrollable_image_preview); +        return imagePreview instanceof ImagePreviewView +                ? ((ImagePreviewView) imagePreview).getTransitionView() +                : null; +    } + +    /** +     * Wrapping the ContentResolver call to expose for easier mocking, +     * and to avoid mocking Android core classes. +     */ +    @VisibleForTesting +    public Cursor queryResolver(ContentResolver resolver, Uri uri) { +        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.clear(); +    } + +    @Override // ResolverListCommunicator +    public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { +        ChooserRequestParameters chooserRequest = getChooserRequest(); +        if (chooserRequest == null) { +            return defIntent; +        } + +        Intent result = defIntent; +        if (chooserRequest.getReplacementExtras() != null) { +            final Bundle replExtras = +                    chooserRequest.getReplacementExtras().getBundle(aInfo.packageName); +            if (replExtras != null) { +                result = new Intent(defIntent); +                result.putExtras(replExtras); +            } +        } +        if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT) +                || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { +            result = Intent.createChooser(result, +                    getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); + +            // Don't auto-launch single intents if the intent is being forwarded. This is done +            // because automatically launching a resolving application as a response to the user +            // action of switching accounts is pretty unexpected. +            result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); +        } +        return result; +    } + +    @Override +    public void onActivityStarted(TargetInfo cti) { +        ChooserRequestParameters chooserRequest = requireChooserRequest(); +        if (chooserRequest.getChosenComponentSender() != 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); +                } +            } +        } +    } + +    private void addCallerChooserTargets() { +        ChooserRequestParameters chooserRequest = requireChooserRequest(); +        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) { +                mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( +                        /* origTarget */ null, +                        new ArrayList<>(chooserRequest.getCallerChooserTargets()), +                        TARGET_TYPE_DEFAULT, +                        /* directShareShortcutInfoCache */ Collections.emptyMap(), +                        /* directShareAppTargetCache */ Collections.emptyMap()); +            } +        } +    } + +    @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)) { +            return false; +        } + +        return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); +    } + +    private void showTargetDetails(TargetInfo targetInfo) { +        if (targetInfo == null) return; + +        List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets(); +        if (targetList.isEmpty()) { +            Log.e(TAG, "No displayable data to show target details"); +            return; +        } + +        // 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; +        String shortcutTitle = targetInfo.isSelectableTargetInfo() +                ? targetInfo.getDisplayLabel().toString() : null; +        String shortcutIdKey = targetInfo.getDirectShareShortcutId(); + +        ChooserTargetActionsDialogFragment.show( +                getSupportFragmentManager(), +                targetList, +                // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be +                // resolved correctly within the same tab. +                targetInfo.getResolveInfo().userHandle, +                shortcutIdKey, +                shortcutTitle, +                isShortcutPinned, +                intentFilter); +    } + +    @Override +    protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { +        if (mRefinementManager.maybeHandleSelection( +                target, +                requireChooserRequest().getRefinementIntentSender(), +                getApplication(), +                getMainThreadHandler())) { +            return false; +        } +        updateModelAndChooserCounts(target); +        maybeRemoveSharedText(target); +        return super.onTargetSelected(target, alwaysCheck); +    } + +    @Override +    public void startSelected(int which, boolean always, boolean filtered) { +        ChooserListAdapter currentListAdapter = +                mChooserMultiProfilePagerAdapter.getActiveListAdapter(); +        TargetInfo targetInfo = currentListAdapter +                .targetInfoForPosition(which, filtered); +        if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) { +            return; +        } + +        final long selectionCost = System.currentTimeMillis() - mChooserShownTime; + +        if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) { +            MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; +            if (!mti.hasSelected()) { +                // Add userHandle based badge to the stackedAppDialogBox. +                ChooserStackedAppDialogFragment.show( +                        getSupportFragmentManager(), +                        mti, +                        which, +                        targetInfo.getResolveInfo().userHandle); +                return; +            } +        } + +        super.startSelected(which, always, filtered); + +        // 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 +        // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably* +        // expected to be null only at out-of-bounds indexes where `getPositionTargetType()` +        // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't +        // need to null-check targetInfo. We only need the null check if it's possible that +        // the ChooserListAdapter contains null elements "in the middle" of its list data, +        // such that they're classified as belonging to one of the real target types. That +        // should probably never happen. But why would this method ever be invoked with a +        // null target at all? Even an out-of-bounds index should never be "selected"... +        if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) { +            switch (currentListAdapter.getPositionTargetType(which)) { +                case ChooserListAdapter.TARGET_SERVICE: +                    getEventLog().logShareTargetSelected( +                            EventLog.SELECTION_TYPE_SERVICE, +                            targetInfo.getResolveInfo().activityInfo.processName, +                            which, +                            /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), +                            requireChooserRequest().getCallerChooserTargets().size(), +                            targetInfo.getHashedTargetIdForMetrics(this), +                            targetInfo.isPinned(), +                            mIsSuccessfullySelected, +                            selectionCost +                    ); +                    return; +                case ChooserListAdapter.TARGET_CALLER: +                case ChooserListAdapter.TARGET_STANDARD: +                    getEventLog().logShareTargetSelected( +                            EventLog.SELECTION_TYPE_APP, +                            targetInfo.getResolveInfo().activityInfo.processName, +                            (which - currentListAdapter.getSurfacedTargetInfo().size()), +                            /* directTargetAlsoRanked= */ -1, +                            currentListAdapter.getCallerTargetCount(), +                            /* directTargetHashed= */ null, +                            targetInfo.isPinned(), +                            mIsSuccessfullySelected, +                            selectionCost +                    ); +                    return; +                case ChooserListAdapter.TARGET_STANDARD_AZ: +                    // A-Z targets are unranked standard targets; we use a value of -1 to mark that +                    // they are from the alphabetical pool. +                    // TODO: why do we log a different selection type if the -1 value already +                    // designates the same condition? +                    getEventLog().logShareTargetSelected( +                            EventLog.SELECTION_TYPE_STANDARD, +                            targetInfo.getResolveInfo().activityInfo.processName, +                            /* value= */ -1, +                            /* directTargetAlsoRanked= */ -1, +                            /* numCallerProvided= */ 0, +                            /* directTargetHashed= */ null, +                            /* isPinned= */ false, +                            mIsSuccessfullySelected, +                            selectionCost +                    ); +                    return; +            } +        } +    } + +    private int getRankedPosition(TargetInfo targetInfo) { +        String targetPackageName = +                targetInfo.getChooserTargetComponentName().getPackageName(); +        ChooserListAdapter currentListAdapter = +                mChooserMultiProfilePagerAdapter.getActiveListAdapter(); +        int maxRankedResults = Math.min( +                currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION); + +        for (int i = 0; i < maxRankedResults; i++) { +            if (currentListAdapter.getDisplayResolveInfo(i) +                    .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) { +                return i; +            } +        } +        return -1; +    } + +    @Override +    protected boolean shouldAddFooterView() { +        // To accommodate for window insets +        return true; +    } + +    @Override +    protected void applyFooterView(int height) { +        mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height); +    } + +    private void logDirectShareTargetReceived(UserHandle forUser) { +        ProfileRecord profileRecord = getProfileRecord(forUser); +        if (profileRecord == null) { +            return; +        } +        getEventLog().logDirectShareTargetReceived( +                MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, +                (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime)); +    } + +    void updateModelAndChooserCounts(TargetInfo info) { +        if (info != null && info.isMultiDisplayResolveInfo()) { +            info = ((MultiDisplayResolveInfo) info).getSelectedTarget(); +        } +        if (info != null) { +            sendClickToAppPredictor(info); +            final ResolveInfo ri = info.getResolveInfo(); +            Intent targetIntent = mLogic.getTargetIntent(); +            if (ri != null && ri.activityInfo != null && targetIntent != null) { +                ChooserListAdapter currentListAdapter = +                        mChooserMultiProfilePagerAdapter.getActiveListAdapter(); +                if (currentListAdapter != null) { +                    sendImpressionToAppPredictor(info, currentListAdapter); +                    currentListAdapter.updateModel(info); +                    currentListAdapter.updateChooserCounts( +                            ri.activityInfo.packageName, +                            targetIntent.getAction(), +                            ri.userHandle); +                } +                if (DEBUG) { +                    Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); +                    Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); +                } +            } else if (DEBUG) { +                Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo"); +            } +        } +        mIsSuccessfullySelected = true; +    } + +    private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) { +        Intent targetIntent = targetInfo.getTargetIntent(); +        if (targetIntent == null) { +            return; +        } +        Intent originalTargetIntent = new Intent(requireChooserRequest().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) { +            originalTargetIntent.setComponent(targetIntent.getComponent()); +        } +        // Use filterEquals as a way to check that the primary intent is in use (and not an +        // alternative one). For example, an app is sharing an image and a link with mime type +        // "image/png" and provides an alternative intent to share only the link with mime type +        // "text/uri". Should there be a target that accepts only the latter, the alternative intent +        // will be used and we don't want to exclude the link from it. +        if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) { +            targetIntent.removeExtra(Intent.EXTRA_TEXT); +        } +    } + +    private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { +        // Send DS target impression info to AppPredictor, only when user chooses app share. +        if (targetInfo.isChooserTargetInfo()) { +            return; +        } + +        AppPredictor directShareAppPredictor = getAppPredictor( +                mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); +        if (directShareAppPredictor == null) { +            return; +        } +        List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo(); +        List<AppTargetId> targetIds = new ArrayList<>(); +        for (TargetInfo chooserTargetInfo : surfacedTargetInfo) { +            ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo(); +            if (shortcutInfo != null) { +                ComponentName componentName = +                        chooserTargetInfo.getChooserTargetComponentName(); +                targetIds.add(new AppTargetId( +                        String.format( +                                "%s/%s/%s", +                                shortcutInfo.getId(), +                                componentName.flattenToString(), +                                SHORTCUT_TARGET))); +            } +        } +        directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); +    } + +    private void sendClickToAppPredictor(TargetInfo targetInfo) { +        if (!targetInfo.isChooserTargetInfo()) { +            return; +        } + +        AppPredictor directShareAppPredictor = getAppPredictor( +                mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); +        if (directShareAppPredictor == null) { +            return; +        } +        AppTarget appTarget = targetInfo.getDirectShareAppTarget(); +        if (appTarget != null) { +            // This is a direct share click that was provided by the APS +            directShareAppPredictor.notifyAppTargetEvent( +                    new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) +                        .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) +                        .build()); +        } +    } + +    @Nullable +    private AppPredictor getAppPredictor(UserHandle userHandle) { +        ProfileRecord record = getProfileRecord(userHandle); +        // We cannot use APS service when clone profile is present as APS service cannot sort +        // cross profile targets as of now. +        return ((record == null) || (requireAnnotatedUserHandles().cloneProfileUserHandle != null)) +                ? 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, +            List<Intent> payloadIntents, +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            UserHandle userHandle, +            TargetDataLoader targetDataLoader) { +        ChooserRequestParameters parameters = requireChooserRequest(); +        ChooserListAdapter chooserListAdapter = createChooserListAdapter( +                context, +                payloadIntents, +                initialIntents, +                rList, +                filterLastUsed, +                createListController(userHandle), +                userHandle, +                mLogic.getTargetIntent(), +                parameters.getReferrerFillInIntent(), +                mMaxTargetsPerRow, +                targetDataLoader); + +        return new ChooserGridAdapter( +                context, +                new ChooserGridAdapter.ChooserActivityDelegate() { +                    @Override +                    public boolean shouldShowTabs() { +                        return ChooserActivity.this.shouldShowTabs(); +                    } + +                    @Override +                    public View buildContentPreview(ViewGroup parent) { +                        return createContentPreviewView(parent); +                    } + +                    @Override +                    public void onTargetSelected(int itemIndex) { +                        startSelected(itemIndex, false, true); +                    } + +                    @Override +                    public void onTargetLongPressed(int selectedPosition) { +                        final TargetInfo longPressedTargetInfo = +                                mChooserMultiProfilePagerAdapter +                                .getActiveListAdapter() +                                .targetInfoForPosition( +                                        selectedPosition, /* filtered= */ true); +                        // Only a direct share target or an app target is expected +                        if (longPressedTargetInfo.isDisplayResolveInfo() +                                || longPressedTargetInfo.isSelectableTargetInfo()) { +                            showTargetDetails(longPressedTargetInfo); +                        } +                    } + +                    @Override +                    public void updateProfileViewButton(View newButtonFromProfileRow) { +                        mProfileView = newButtonFromProfileRow; +                        mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); +                        ChooserActivity.this.updateProfileViewButton(); +                    } +                }, +                chooserListAdapter, +                shouldShowContentPreview(), +                mMaxTargetsPerRow, +                mFeatureFlags); +    } + +    @VisibleForTesting +    public ChooserListAdapter createChooserListAdapter( +            Context context, +            List<Intent> payloadIntents, +            Intent[] initialIntents, +            List<ResolveInfo> rList, +            boolean filterLastUsed, +            ResolverListController resolverListController, +            UserHandle userHandle, +            Intent targetIntent, +            Intent referrerFillInIntent, +            int maxTargetsPerRow, +            TargetDataLoader targetDataLoader) { +        UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() +                && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle) +                ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle; +        return new ChooserListAdapter( +                context, +                payloadIntents, +                initialIntents, +                rList, +                filterLastUsed, +                createListController(userHandle), +                userHandle, +                targetIntent, +                referrerFillInIntent, +                this, +                context.getPackageManager(), +                getEventLog(), +                maxTargetsPerRow, +                initialIntentsUserSpace, +                targetDataLoader, +                () -> { +                    ProfileRecord record = getProfileRecord(userHandle); +                    if (record != null && record.shortcutLoader != null) { +                        record.shortcutLoader.reset(); +                    } +                }); +    } + +    @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(); +    } + +    @Override +    @VisibleForTesting +    protected ChooserListController createListController(UserHandle userHandle) { +        AppPredictor appPredictor = getAppPredictor(userHandle); +        AbstractResolverComparator resolverComparator; +        if (appPredictor != null) { +            resolverComparator = new AppPredictionServiceResolverComparator( +                    this, +                    mLogic.getTargetIntent(), +                    mLogic.getReferrerPackageName(), +                    appPredictor, +                    userHandle, +                    getEventLog(), +                    mNearbyShare.orElse(null) +            ); +        } else { +            resolverComparator = +                    new ResolverRankerServiceResolverComparator( +                            this, +                            mLogic.getTargetIntent(), +                            mLogic.getReferrerPackageName(), +                            null, +                            getEventLog(), +                            getResolverRankerServiceUserHandleList(userHandle), +                            mNearbyShare.orElse(null)); +        } + +        return new ChooserListController( +                this, +                mPm, +                mLogic.getTargetIntent(), +                mLogic.getReferrerPackageName(), +                requireAnnotatedUserHandles().userIdOfCallingApp, +                resolverComparator, +                getQueryIntentsUser(userHandle)); +    } + +    @VisibleForTesting +    protected ViewModelProvider.Factory createPreviewViewModelFactory() { +        return PreviewViewModel.Companion.getFactory(); +    } + +    private ChooserActionFactory createChooserActionFactory() { +        ChooserRequestParameters request = requireChooserRequest(); +        return new ChooserActionFactory( +                this, +                request.getTargetIntent(), +                request.getReferrerPackageName(), +                request.getChooserActions(), +                request.getModifyShareAction(), +                mImageEditor, +                getEventLog(), +                (isExcluded) -> mExcludeSharedText = isExcluded, +                this::getFirstVisibleImgPreviewView, +                new ChooserActionFactory.ActionActivityStarter() { +                    @Override +                    public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { +                        safelyStartActivityAsUser( +                                targetInfo, +                                requireAnnotatedUserHandles().personalProfileUserHandle +                        ); +                        finish(); +                    } + +                    @Override +                    public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( +                            TargetInfo targetInfo, View sharedElement, String sharedElementName) { +                        ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( +                                ChooserActivity.this, sharedElement, sharedElementName); +                        safelyStartActivityAsUser( +                                targetInfo, +                                requireAnnotatedUserHandles().personalProfileUserHandle, +                                options.toBundle()); +                        // Can't finish right away because the shared element transition may not +                        // be ready to start. +                        mFinishWhenStopped = true; +                    } +                }, +                (status) -> { +                    if (status != null) { +                        setResult(status); +                    } +                    finish(); +                }); +    } + +    /* +     * Need to dynamically adjust how many icons can fit per row before we add them, +     * which also means setting the correct offset to initially show the content +     * preview area + 2 rows of targets +     */ +    private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, +            int oldTop, int oldRight, int oldBottom) { +        if (mChooserMultiProfilePagerAdapter == null) { +            return; +        } +        RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); +        ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); +        // Skip height calculation if recycler view was scrolled to prevent it inaccurately +        // calculating the height, as the logic below does not account for the scrolled offset. +        if (gridAdapter == null || recyclerView == null +                || recyclerView.computeVerticalScrollOffset() != 0) { +            return; +        } + +        final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight(); +        boolean isLayoutUpdated = +                gridAdapter.calculateChooserTargetWidth(availableWidth) +                || recyclerView.getAdapter() == null +                || availableWidth != mCurrAvailableWidth; + +        boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets); + +        if (isLayoutUpdated +                || insetsChanged +                || mLastNumberOfChildren != recyclerView.getChildCount()) { +            mCurrAvailableWidth = availableWidth; +            if (isLayoutUpdated) { +                // It is very important we call setAdapter from here. Otherwise in some cases +                // the resolver list doesn't get populated, such as b/150922090, b/150918223 +                // and b/150936654 +                recyclerView.setAdapter(gridAdapter); +                ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount( +                        mMaxTargetsPerRow); + +                updateTabPadding(); +            } + +            UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle(); +            int currentProfile = getProfileForUser(currentUserHandle); +            int initialProfile = findSelectedProfile(); +            if (currentProfile != initialProfile) { +                return; +            } + +            if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) { +                return; +            } + +            getMainThreadHandler().post(() -> { +                if (mResolverDrawerLayout == null || gridAdapter == null) { +                    return; +                } +                int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter); +                mResolverDrawerLayout.setCollapsibleHeightReserved(offset); +                mEnterTransitionAnimationDelegate.markOffsetCalculated(); +                mLastAppliedInsets = mSystemWindowInsets; +            }); +        } +    } + +    private int calculateDrawerOffset( +            int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) { + +        int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; +        int rowsToShow = gridAdapter.getSystemRowCount() +                + gridAdapter.getProfileRowCount() +                + gridAdapter.getServiceTargetRowCount() +                + gridAdapter.getCallerAndRankedTargetRowCount(); + +        // then this is most likely not a SEND_* action, so check +        // the app target count +        if (rowsToShow == 0) { +            rowsToShow = gridAdapter.getRowCount(); +        } + +        // still zero? then use a default height and leave, which +        // can happen when there are no targets to show +        if (rowsToShow == 0 && !shouldShowStickyContentPreview()) { +            offset += getResources().getDimensionPixelSize( +                    R.dimen.chooser_max_collapsed_height); +            return offset; +        } + +        View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container); +        if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) { +            offset += stickyContentPreview.getHeight(); +        } + +        if (shouldShowTabs()) { +            offset += findViewById(com.android.internal.R.id.tabs).getHeight(); +        } + +        if (recyclerView.getVisibility() == View.VISIBLE) { +            rowsToShow = Math.min(4, rowsToShow); +            boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); +            mLastNumberOfChildren = recyclerView.getChildCount(); +            for (int i = 0, childCount = recyclerView.getChildCount(); +                    i < childCount && rowsToShow > 0; i++) { +                View child = recyclerView.getChildAt(i); +                if (((GridLayoutManager.LayoutParams) +                        child.getLayoutParams()).getSpanIndex() != 0) { +                    continue; +                } +                int height = child.getHeight(); +                offset += height; +                if (shouldShowExtraRow) { +                    offset += height; +                } +                rowsToShow--; +            } +        } else { +            ViewGroup currentEmptyStateView = +                    mChooserMultiProfilePagerAdapter.getActiveEmptyStateView(); +            if (currentEmptyStateView.getVisibility() == View.VISIBLE) { +                offset += currentEmptyStateView.getHeight(); +            } +        } + +        return Math.min(offset, bottom - top); +    } + +    /** +     * If we have a tabbed view and are showing 1 row in the current profile and an empty +     * state screen in another profile, to prevent cropping of the empty state screen we show +     * a second row in the current profile. +     */ +    private boolean shouldShowExtraRow(int rowsToShow) { +        return rowsToShow == 1 +                && mChooserMultiProfilePagerAdapter +                        .shouldShowEmptyStateScreenInAnyInactiveAdapter(); +    } + +    /** +     * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle. +     * Returns {@link #PROFILE_PERSONAL}, otherwise. +     **/ +    private int getProfileForUser(UserHandle currentUserHandle) { +        if (currentUserHandle.equals(requireAnnotatedUserHandles().workProfileUserHandle)) { +            return PROFILE_WORK; +        } +        // We return personal profile, as it is the default when there is no work profile, personal +        // profile represents rootUser, clonedUser & secondaryUser, covering all use cases. +        return PROFILE_PERSONAL; +    } + +    @Override +    protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { +        setupScrollListener(); +        maybeSetupGlobalLayoutListener(); + +        ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; +        UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle(); +        if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) { +            mChooserMultiProfilePagerAdapter.getActiveAdapterView() +                    .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()); +            mChooserMultiProfilePagerAdapter +                    .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage()); +        } + +        //TODO: move this block inside ChooserListAdapter (should be called when +        // ResolverListAdapter#mPostListReadyRunnable is executed. +        if (chooserListAdapter.getDisplayResolveInfoCount() == 0) { +            chooserListAdapter.notifyDataSetChanged(); +        } else { +            chooserListAdapter.updateAlphabeticalList(); +        } + +        if (rebuildComplete) { +            long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle); +            if (duration >= 0) { +                Log.d(TAG, "app target loading time " + duration + " ms"); +            } +            addCallerChooserTargets(); +            getEventLog().logSharesheetAppLoadComplete(); +            maybeQueryAdditionalPostProcessingTargets( +                    listProfileUserHandle, +                    chooserListAdapter.getDisplayResolveInfos()); +            mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET); +        } +    } + +    private void maybeQueryAdditionalPostProcessingTargets( +            UserHandle userHandle, +            DisplayResolveInfo[] displayResolveInfos) { +        ProfileRecord record = getProfileRecord(userHandle); +        if (record == null || record.shortcutLoader == null) { +            return; +        } +        record.loadingStartTime = SystemClock.elapsedRealtime(); +        record.shortcutLoader.updateAppTargets(displayResolveInfos); +    } + +    @MainThread +    private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) { +        if (DEBUG) { +            Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); +        } +        mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache()); +        mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache()); +        ChooserListAdapter adapter = +                mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); +        if (adapter != null) { +            for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) { +                adapter.addServiceResults( +                        resultInfo.getAppTarget(), +                        resultInfo.getShortcuts(), +                        result.isFromAppPredictor() +                                ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE +                                : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, +                        mDirectShareShortcutInfoCache, +                        mDirectShareAppTargetCache); +            } +            adapter.completeServiceTargetLoading(); +        } + +        if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { +            long duration = Tracer.INSTANCE.endLaunchToShortcutTrace(); +            if (duration >= 0) { +                Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); +            } +        } +        logDirectShareTargetReceived(userHandle); +        sendVoiceChoicesIfNeeded(); +        getEventLog().logSharesheetDirectLoadComplete(); +    } + +    private void setupScrollListener() { +        if (mResolverDrawerLayout == null) { +            return; +        } +        int elevatedViewResId = shouldShowTabs() ? 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 = +                getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); +        mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener( +                new RecyclerView.OnScrollListener() { +                    @Override +                    public void onScrollStateChanged(RecyclerView view, int scrollState) { +                        if (scrollState == RecyclerView.SCROLL_STATE_IDLE) { +                            if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) { +                                mScrollStatus = SCROLL_STATUS_IDLE; +                                setHorizontalScrollingEnabled(true); +                            } +                        } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) { +                            if (mScrollStatus == SCROLL_STATUS_IDLE) { +                                mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL; +                                setHorizontalScrollingEnabled(false); +                            } +                        } +                    } + +                    @Override +                    public void onScrolled(RecyclerView view, int dx, int dy) { +                        if (view.getChildCount() > 0) { +                            View child = view.getLayoutManager().findViewByPosition(0); +                            if (child == null || child.getTop() < 0) { +                                elevatedView.setElevation(chooserHeaderScrollElevation); +                                return; +                            } +                        } + +                        elevatedView.setElevation(defaultElevation); +                    } +                }); +    } + +    private void maybeSetupGlobalLayoutListener() { +        if (shouldShowTabs()) { +            return; +        } +        final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); +        recyclerView.getViewTreeObserver() +                .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { +                    @Override +                    public void onGlobalLayout() { +                        // Fixes an issue were the accessibility border disappears on list creation. +                        recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); +                        final TextView titleView = findViewById(com.android.internal.R.id.title); +                        if (titleView != null) { +                            titleView.setFocusable(true); +                            titleView.setFocusableInTouchMode(true); +                            titleView.requestFocus(); +                            titleView.requestAccessibilityFocus(); +                        } +                    } +                }); +    } + +    /** +     * The sticky content preview is shown only when we have a tabbed view. It's shown above +     * the tabs so it is not part of the scrollable list. If we are not in tabbed view, +     * we instead show the content preview as a regular list item. +     */ +    private boolean shouldShowStickyContentPreview() { +        return shouldShowStickyContentPreviewNoOrientationCheck(); +    } + +    private boolean shouldShowStickyContentPreviewNoOrientationCheck() { +        if (!shouldShowContentPreview()) { +            return false; +        } +        boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle( +                UserHandle.of(UserHandle.myUserId())).getCount() == 0; +        return (mFeatureFlags.scrollablePreview() || shouldShowTabs()) +                && (!isEmpty || shouldShowContentPreviewWhenEmpty()); +    } + +    /** +     * This method could be used to override the default behavior when we hide the preview area +     * when the current tab doesn't have any items. +     * +     * @return true if we want to show the content preview area even if the tab for the current +     *         user is empty +     */ +    protected boolean shouldShowContentPreviewWhenEmpty() { +        return false; +    } + +    /** +     * @return true if we want to show the content preview area +     */ +    protected boolean shouldShowContentPreview() { +        ChooserRequestParameters chooserRequest = getChooserRequest(); +        return (chooserRequest != null) && chooserRequest.isSendActionTarget(); +    } + +    private void updateStickyContentPreview() { +        if (shouldShowStickyContentPreviewNoOrientationCheck()) { +            // The sticky content preview is only shown when we show the work and personal tabs. +            // We don't show it in landscape as otherwise there is no room for scrolling. +            // If the sticky content preview will be shown at some point with orientation change, +            // then always preload it to avoid subsequent resizing of the share sheet. +            ViewGroup contentPreviewContainer = +                    findViewById(com.android.internal.R.id.content_preview_container); +            if (contentPreviewContainer.getChildCount() == 0) { +                ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer); +                contentPreviewContainer.addView(contentPreviewView); +            } +        } +        if (shouldShowStickyContentPreview()) { +            showStickyContentPreview(); +        } else { +            hideStickyContentPreview(); +        } +    } + +    private void showStickyContentPreview() { +        if (isStickyContentPreviewShowing()) { +            return; +        } +        ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); +        contentPreviewContainer.setVisibility(View.VISIBLE); +    } + +    private boolean isStickyContentPreviewShowing() { +        ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); +        return contentPreviewContainer.getVisibility() == View.VISIBLE; +    } + +    private void hideStickyContentPreview() { +        if (!isStickyContentPreviewShowing()) { +            return; +        } +        ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container); +        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() { +        // 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 +        setVerticalScrollEnabled(true); +        if (mResolverDrawerLayout != null) { +            mResolverDrawerLayout.scrollNestedScrollableChildBackToTop(); +        } +    } + +    @Override +    protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { +        if (shouldShowTabs()) { +            mChooserMultiProfilePagerAdapter +                    .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom()); +        } + +        WindowInsets result = super.onApplyWindowInsets(v, insets); +        if (mResolverDrawerLayout != null) { +            mResolverDrawerLayout.requestLayout(); +        } +        return result; +    } + +    private void setHorizontalScrollingEnabled(boolean enabled) { +        ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); +        viewPager.setSwipingEnabled(enabled); +    } + +    private void setVerticalScrollEnabled(boolean enabled) { +        ChooserGridLayoutManager layoutManager = +                (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView() +                        .getLayoutManager(); +        layoutManager.setVerticalScrollEnabled(enabled); +    } + +    @Override +    void onHorizontalSwipeStateChanged(int state) { +        if (state == ViewPager.SCROLL_STATE_DRAGGING) { +            if (mScrollStatus == SCROLL_STATUS_IDLE) { +                mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL; +                setVerticalScrollEnabled(false); +            } +        } else if (state == ViewPager.SCROLL_STATE_IDLE) { +            if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) { +                mScrollStatus = SCROLL_STATUS_IDLE; +                setVerticalScrollEnabled(true); +            } +        } +    } + +    @Override +    protected void maybeLogProfileChange() { +        getEventLog().logSharesheetProfileChanged(); +    } + +    private static class ProfileRecord { +        /** The {@link AppPredictor} for this profile, if any. */ +        @Nullable +        public final AppPredictor appPredictor; +        /** +         * null if we should not load shortcuts. +         */ +        @Nullable +        public final ShortcutLoader shortcutLoader; +        public long loadingStartTime; + +        private ProfileRecord( +                @Nullable AppPredictor appPredictor, +                @Nullable ShortcutLoader shortcutLoader) { +            this.appPredictor = appPredictor; +            this.shortcutLoader = shortcutLoader; +        } + +        public void destroy() { +            if (appPredictor != null) { +                appPredictor.destroy(); +            } +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt new file mode 100644 index 00000000..7bc39a24 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt @@ -0,0 +1,87 @@ +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. + */ +@OpenForTesting +open class ChooserActivityLogic( +    tag: String, +    activityProvider: () -> ComponentActivity, +    onWorkProfileStatusUpdated: () -> Unit, +    targetDataLoaderProvider: () -> TargetDataLoader, +    private val onPreInitialization: () -> Unit, +) : +    ActivityLogic, +    CommonActivityLogic by CommonActivityLogicImpl( +        tag, +        activityProvider, +        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/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java new file mode 100644 index 00000000..de0a9426 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2; + +import android.content.Context; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.PagerAdapter; + +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate; +import com.android.intentresolver.FeatureFlags; +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; + +import java.util.Optional; +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; + +    private final ChooserProfileAdapterBinder mAdapterBinder; +    private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; + +    public ChooserMultiProfilePagerAdapter( +            Context context, +            ChooserGridAdapter adapter, +            EmptyStateProvider emptyStateProvider, +            Supplier<Boolean> workProfileQuietModeChecker, +            UserHandle workProfileUserHandle, +            UserHandle cloneProfileUserHandle, +            int maxTargetsPerRow, +            FeatureFlags featureFlags) { +        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), +                emptyStateProvider, +                workProfileQuietModeChecker, +                defaultProfile, +                workProfileUserHandle, +                cloneProfileUserHandle, +                new BottomPaddingOverrideSupplier(context), +                featureFlags); +    } + +    private ChooserMultiProfilePagerAdapter( +            Context context, +            ChooserProfileAdapterBinder adapterBinder, +            ImmutableList<ChooserGridAdapter> gridAdapters, +            EmptyStateProvider emptyStateProvider, +            Supplier<Boolean> workProfileQuietModeChecker, +            @Profile int defaultProfile, +            UserHandle workProfileUserHandle, +            UserHandle cloneProfileUserHandle, +            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, +            FeatureFlags featureFlags) { +        super( +                        gridAdapter -> gridAdapter.getListAdapter(), +                adapterBinder, +                gridAdapters, +                emptyStateProvider, +                workProfileQuietModeChecker, +                defaultProfile, +                workProfileUserHandle, +                cloneProfileUserHandle, +                        () -> makeProfileView(context, featureFlags), +                bottomPaddingOverrideSupplier); +        mAdapterBinder = adapterBinder; +        mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; +    } + +    public void setMaxTargetsPerRow(int maxTargetsPerRow) { +        mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow); +    } + +    public void setEmptyStateBottomOffset(int bottomOffset) { +        mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset); +        setupContainerPadding(); +    } + +    /** +     * Notify adapter about the drawer's collapse state. This will affect the app divider's +     * visibility. +     */ +    public void setIsCollapsed(boolean isCollapsed) { +        for (int i = 0, size = getItemCount(); i < size; i++) { +            getAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); +        } +    } + +    private static ViewGroup makeProfileView( +            Context context, FeatureFlags featureFlags) { +        LayoutInflater inflater = LayoutInflater.from(context); +        ViewGroup rootView = featureFlags.scrollablePreview() +                ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) +                : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); +        RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); +        recyclerView.setAccessibilityDelegateCompat( +                new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); +        return rootView; +    } + +    @Override +    public boolean onHandlePackagesChanged( +            ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { +        // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? +        getActiveListAdapter().notifyDataSetChanged(); +        return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); +    } + +    @Override +    protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { +        if (doPostProcessing) { +            Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); +        } +        return super.rebuildTab(listAdapter, doPostProcessing); +    } + +    /** 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); +        } +    } + +    private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> { +        private final Context mContext; +        private int mBottomOffset; + +        BottomPaddingOverrideSupplier(Context context) { +            mContext = context; +        } + +        public void setEmptyStateBottomOffset(int bottomOffset) { +            mBottomOffset = bottomOffset; +        } + +        @Override +        public Optional<Integer> get() { +            int initialBottomPadding = mContext.getResources().getDimensionPixelSize( +                    R.dimen.resolver_empty_state_container_padding_bottom); +            return Optional.of(initialBottomPadding + mBottomOffset); +        } +    } + +    private static class ChooserProfileAdapterBinder implements +            AdapterBinder<RecyclerView, ChooserGridAdapter> { +        private int mMaxTargetsPerRow; + +        ChooserProfileAdapterBinder(int maxTargetsPerRow) { +            mMaxTargetsPerRow = maxTargetsPerRow; +        } + +        public void setMaxTargetsPerRow(int maxTargetsPerRow) { +            mMaxTargetsPerRow = maxTargetsPerRow; +        } + +        @Override +        public void bind( +                RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) { +            GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager(); +            glm.setSpanCount(mMaxTargetsPerRow); +            glm.setSpanSizeLookup( +                    new GridLayoutManager.SpanSizeLookup() { +                        @Override +                        public int getSpanSize(int position) { +                            return chooserGridAdapter.shouldCellSpan(position) +                                    ? SINGLE_CELL_SPAN_SIZE +                                    : glm.getSpanCount(); +                        } +                    }); +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/v2/ChooserSelector.kt new file mode 100644 index 00000000..378bc06c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ChooserSelector.kt @@ -0,0 +1,36 @@ +package com.android.intentresolver.v2 + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import com.android.intentresolver.FeatureFlags +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint(BroadcastReceiver::class) +class ChooserSelector : Hilt_ChooserSelector() { + +    @Inject lateinit var featureFlags: FeatureFlags + +    override fun onReceive(context: Context, intent: Intent) { +        super.onReceive(context, intent) +        if (intent.action == Intent.ACTION_BOOT_COMPLETED) { +            context.packageManager.setComponentEnabledSetting( +                ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS), +                if (featureFlags.modularFramework()) { +                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED +                } else { +                    PackageManager.COMPONENT_ENABLED_STATE_DEFAULT +                }, +                /* flags = */ 0, +            ) +        } +    } + +    companion object { +        private const val CHOOSER_PACKAGE = "com.android.intentresolver" +        private const val CHOOSER_CLASS = ".v2.ChooserActivity" +    } +} diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java new file mode 100644 index 00000000..2d9be816 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java @@ -0,0 +1,666 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.os.Trace; +import android.os.UserHandle; +import android.view.View; +import android.view.ViewGroup; + +import androidx.viewpager.widget.PagerAdapter; +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.Optional; +import java.util.Set; +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 + * the per-profile records. + * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to + * control the contents of a given per-profile list. This is provided for convenience, since it must + * be possible to get the list adapter from the page adapter via our + * <code>mListAdapterExtractor</code>. + */ +public class MultiProfilePagerAdapter< +        PageViewT extends ViewGroup, +        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 {} + +    private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor; +    private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder; +    private final Supplier<ViewGroup> mPageViewInflater; + +    private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems; + +    private final EmptyStateProvider mEmptyStateProvider; +    private final UserHandle mWorkProfileUserHandle; +    private final UserHandle mCloneProfileUserHandle; +    private final Supplier<Boolean> mWorkProfileQuietModeChecker;  // True when work is quiet. + +    private Set<Integer> mLoadedPages; +    private int mCurrentPage; +    private OnProfileSelectedListener mOnProfileSelectedListener; + +    protected MultiProfilePagerAdapter( +            Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor, +            AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, +            ImmutableList<SinglePageAdapterT> adapters, +            EmptyStateProvider emptyStateProvider, +            Supplier<Boolean> workProfileQuietModeChecker, +            @Profile int defaultProfile, +            UserHandle workProfileUserHandle, +            UserHandle cloneProfileUserHandle, +            Supplier<ViewGroup> pageViewInflater, +            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { +        mCurrentPage = defaultProfile; +        mLoadedPages = new HashSet<>(); +        mWorkProfileUserHandle = workProfileUserHandle; +        mCloneProfileUserHandle = cloneProfileUserHandle; +        mEmptyStateProvider = emptyStateProvider; +        mWorkProfileQuietModeChecker = workProfileQuietModeChecker; + +        mListAdapterExtractor = listAdapterExtractor; +        mAdapterBinder = adapterBinder; +        mPageViewInflater = pageViewInflater; + +        ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items = +                new ImmutableList.Builder<>(); +        for (SinglePageAdapterT adapter : adapters) { +            items.add(createProfileDescriptor(adapter, containerBottomPaddingOverrideSupplier)); +        } +        mItems = items.build(); +    } + +    private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor( +            SinglePageAdapterT adapter, +            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { +        return new ProfileDescriptor<>( +                mPageViewInflater.get(), adapter, containerBottomPaddingOverrideSupplier); +    } + +    public void setOnProfileSelectedListener(OnProfileSelectedListener listener) { +        mOnProfileSelectedListener = listener; +    } + +    /** +     * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets +     * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed +     * page and rebuilds the list. +     */ +    public void setupViewPager(ViewPager viewPager) { +        viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { +            @Override +            public void onPageSelected(int position) { +                mCurrentPage = position; +                if (!mLoadedPages.contains(position)) { +                    rebuildActiveTab(true); +                    mLoadedPages.add(position); +                } +                if (mOnProfileSelectedListener != null) { +                    mOnProfileSelectedListener.onProfileSelected(position); +                } +            } + +            @Override +            public void onPageScrollStateChanged(int state) { +                if (mOnProfileSelectedListener != null) { +                    mOnProfileSelectedListener.onProfilePageStateChanged(state); +                } +            } +        }); +        viewPager.setAdapter(this); +        viewPager.setCurrentItem(mCurrentPage); +        mLoadedPages.add(mCurrentPage); +    } + +    public void clearInactiveProfileCache() { +        if (mLoadedPages.size() == 1) { +            return; +        } +        mLoadedPages.remove(1 - mCurrentPage); +    } + +    @Override +    public final ViewGroup instantiateItem(ViewGroup container, int position) { +        setupListAdapter(position); +        final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position); +        container.addView(descriptor.mRootView); +        return descriptor.mRootView; +    } + +    @Override +    public void destroyItem(ViewGroup container, int position, Object view) { +        container.removeView((View) view); +    } + +    @Override +    public int getCount() { +        return getItemCount(); +    } + +    public int getCurrentPage() { +        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(); +    } + +    @VisibleForTesting +    public UserHandle getCurrentUserHandle() { +        return getActiveListAdapter().getUserHandle(); +    } + +    @Override +    public boolean isViewFromObject(View view, Object object) { +        return view == object; +    } + +    @Override +    public CharSequence getPageTitle(int position) { +        return null; +    } + +    public UserHandle getCloneUserHandle() { +        return mCloneProfileUserHandle; +    } + +    /** +     * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>. +     * <ul> +     * <li>For a device with only one user, <code>pageIndex</code> value of +     * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li> +     * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would +     * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of +     * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li> +     * </ul> +     */ +    private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) { +        return mItems.get(pageIndex); +    } + +    private ViewGroup getEmptyStateView(int pageIndex) { +        return getItem(pageIndex).getEmptyStateView(); +    } + +    public ViewGroup getActiveEmptyStateView() { +        return getEmptyStateView(getCurrentPage()); +    } + +    /** +     * Returns the number of {@link ProfileDescriptor} objects. +     * <p>For a normal consumer device with only one user returns <code>1</code>. +     * <p>For a device with a work profile returns <code>2</code>. +     */ +    public final int getItemCount() { +        return mItems.size(); +    } + +    public final PageViewT getListViewForIndex(int index) { +        return getItem(index).mView; +    } + +    /** +     * Returns the adapter of the list view for the relevant page specified by +     * <code>pageIndex</code>. +     * <p>This method is meant to be implemented with an implementation-specific return type +     * depending on the adapter type. +     */ +    @VisibleForTesting +    public final SinglePageAdapterT getAdapterForIndex(int index) { +        return getItem(index).mAdapter; +    } + +    /** +     * Performs view-related initialization procedures for the adapter specified +     * 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; +    } + +    /** +     * Returns the {@link ListAdapterT} instance of the profile that is currently visible +     * 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())); +    } + +    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); +    } + +    @Nullable +    public final ListAdapterT getWorkListAdapter() { +        if (!hasAdapterForIndex(PROFILE_WORK)) { +            return null; +        } +        return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); +    } + +    public final SinglePageAdapterT getCurrentRootAdapter() { +        return getAdapterForIndex(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)); +            if (listAdapter.getCount() > 0) { +                return true; +            } +        } +        return false; +    } + +    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. +        getActiveListAdapter().handlePackagesChanged(); +        if (getCount() > 1) { +            getInactiveListAdapter().handlePackagesChanged(); +        } +    } + +    /** +     * Notify that there has been a package change which could potentially modify the set of targets +     * that should be shown in the specified {@code listAdapter}. This <em>may</em> result in +     * "rebuilding" the target list for that adapter. +     * +     * @param listAdapter an adapter that may need to be updated after the package-change event. +     * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet +     * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any +     * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild +     * will be prompted when we eventually get the broadcast. +     * +     * @return whether we're able to proceed with a Sharesheet session after processing this +     * package-change event. If false, we were able to rebuild the targets but determined that there +     * aren't any we could present in the UI without the app looking broken, so we should just quit. +     */ +    public boolean onHandlePackagesChanged( +            ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { +        if (listAdapter == getActiveListAdapter()) { +            if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) +                    && waitingToEnableWorkProfile) { +                // We have just turned on the work profile and entered the passcode to start it, +                // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no +                // point in reloading the list now, since the work profile user is still turning on. +                return true; +            } + +            boolean listRebuilt = rebuildActiveTab(true); +            if (listRebuilt) { +                listAdapter.notifyDataSetChanged(); +            } + +            // TODO: shouldn't we check that the inactive tabs are built before declaring that we +            // have to quit for lack of items? +            return anyAdapterHasItems(); +        } else { +            clearInactiveProfileCache(); +            return true; +        } +    } + +    /** +     * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. +     */ +    public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) { +        // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as +        // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to +        // be able to evaluate the intermediate state of one particular profile tab (i.e. work +        // profile) that may not generalize well when we have other "inactive tabs." I.e., either we +        // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only +        // depend on personal and/or work tabs, or we have to explicitly specify the ones we care +        // about. It's not the pager-adapter's business to know "which ones we care about," so maybe +        // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of +        // autolaunch conditions). +        boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded(); +        if (includePartialRebuildOfInactiveTabs) { +            boolean rebuildInactiveCompleted = +                    rebuildInactiveTab(false) || getInactiveListAdapter().isTabLoaded(); +            rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted; +        } +        return rebuildCompleted; +    } + +    /** +     * Rebuilds the tab that is currently visible to the user. +     * <p>Returns {@code true} if rebuild has completed. +     */ +    public final boolean rebuildActiveTab(boolean doPostProcessing) { +        Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab"); +        boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing); +        Trace.endSection(); +        return result; +    } + +    /** +     * Rebuilds the tab that is not currently visible to the user, if such one exists. +     * <p>Returns {@code true} if rebuild has completed. +     */ +    private boolean rebuildInactiveTab(boolean doPostProcessing) { +        Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); +        if (getItemCount() == 1) { +            Trace.endSection(); +            return false; +        } +        boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing); +        Trace.endSection(); +        return result; +    } + +    private int userHandleToPageIndex(UserHandle userHandle) { +        if (userHandle.equals(getPersonalListAdapter().getUserHandle())) { +            return PROFILE_PERSONAL; +        } else { +            return PROFILE_WORK; +        } +    } + +    protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) { +        if (shouldSkipRebuild(activeListAdapter)) { +            activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); +            return false; +        } +        return activeListAdapter.rebuildList(doPostProcessing); +    } + +    private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) { +        EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); +        return emptyState != null && emptyState.shouldSkipDataRebuild(); +    } + +    private boolean hasAdapterForIndex(int pageIndex) { +        return (pageIndex < getCount()); +    } + +    /** +     * The empty state screens are shown according to their priority: +     * <ol> +     * <li>(highest priority) cross-profile disabled by policy (handled in +     * {@link #rebuildTab(ListAdapterT, boolean)})</li> +     * <li>no apps available</li> +     * <li>(least priority) work is off</li> +     * </ol> +     * +     * The intention is to prevent the user from having to turn +     * the work profile on if there will not be any apps resolved +     * anyway. +     * +     * TODO: move this comment to the place where we configure our composite provider. +     */ +    public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) { +        final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); + +        if (emptyState == null) { +            return; +        } + +        emptyState.onEmptyStateShown(); + +        View.OnClickListener clickListener = null; + +        if (emptyState.getButtonClickListener() != null) { +            clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { +                ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem( +                        userHandleToPageIndex(listAdapter.getUserHandle())); +                descriptor.mEmptyStateUi.showSpinner(); +            }); +        } + +        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())); +        descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick); +        activeListAdapter.markTabLoaded(); +    } + +    /** +     * Sets up the padding of the view containing the empty state screens for the current adapter +     * view. +     */ +    protected final void setupContainerPadding() { +        getItem(getCurrentPage()).setupContainerPadding(); +    } + +    public void showListView(ListAdapterT activeListAdapter) { +        ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem( +                userHandleToPageIndex(activeListAdapter.getUserHandle())); +        descriptor.mEmptyStateUi.hide(); +    } + +    /** +     * @return whether any "inactive" tab's adapter would show an empty-state screen in our current +     * 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()); +    } + +    public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) { +        int count = listAdapter.getUnfilteredCount(); +        return (count == 0 && listAdapter.getPlaceholderCount() == 0) +                || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) +                    && 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/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java new file mode 100644 index 00000000..2ba50ec3 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -0,0 +1,2181 @@ +/* + * Copyright (C) 2008 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 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.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; +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; +import android.os.Bundle; +import android.os.PatternMatcher; +import android.os.RemoteException; +import android.os.StrictMode; +import android.os.Trace; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.stats.devicepolicy.DevicePolicyEnums; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.FrameLayout; +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.viewpager.widget.ViewPager; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.R; +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.WorkProfileAvailabilityManager; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; +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.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.ui.ActionTitle; +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 kotlin.Unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is + * *not* the resolver that is actually triggered by the system right now (you want + * 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 +        ResolverListAdapter.ResolverListCommunicator { + +    private final List<Runnable> mInit = new ArrayList<>(); + +    protected ActivityLogic mLogic; + +    private DevicePolicyResources mDevicePolicyResources; + +    public ResolverActivity() { +        mIsIntentPicker = getClass().equals(ResolverActivity.class); +    } + +    protected ResolverActivity(boolean isIntentPicker) { +        mIsIntentPicker = isIntentPicker; +    } + +    private Button mAlwaysButton; +    private Button mOnceButton; +    protected View mProfileView; +    private int mLastSelected = AbsListView.INVALID_POSITION; +    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; +    private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key"; + +    private boolean mRegistered; + +    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 static final String TAB_TAG_PERSONAL = "personal"; +    private static final String TAB_TAG_WORK = "work"; + +    private PackageMonitor mPersonalPackageMonitor; +    private PackageMonitor mWorkPackageMonitor; + +    @VisibleForTesting +    protected MultiProfilePagerAdapter 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; + +    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 +            public boolean onPackageChanged(String packageName, int uid, String[] components) { +                // We care about all package changes, not just the whole package itself which is +                // default behavior. +                return true; +            } +        }; +    } +    protected interface Initializer { +        void initialize(ActivityLogic value); +    } + +    protected void setLogic(ActivityLogic logic) { +        mLogic = logic; +    } + +    protected void addInitializer(Runnable initializer) { +        mInit.add(initializer); +    } + +    @Override +    protected void onCreate(Bundle savedInstanceState) { +        super.onCreate(savedInstanceState); +        if (isFinishing()) { +            // Performing a clean exit: +            //    Skip initializing anything. +            return; +        } +        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(); +        } +    } + +    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(); + +        // 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 +        // turn this off when running under voice interaction, since it results in +        // a more complicated UI that the current voice interaction flow is not able +        // to handle. We also turn it off when multiple tabs are shown to simplify the UX. +        // 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(); +        mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( +                requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), +                /* resolutionList = */ null, +                filterLastUsed, +                targetDataLoader +        ); +        if (configureContentView(targetDataLoader)) { +            return; +        } + +        mPersonalPackageMonitor = createPackageMonitor( +                mMultiProfilePagerAdapter.getPersonalListAdapter()); +        mPersonalPackageMonitor.register( +                this, +                getMainLooper(), +                requireAnnotatedUserHandles().personalProfileUserHandle, +                false +        ); +        if (hasWorkProfile()) { +            mWorkPackageMonitor = createPackageMonitor( +                    mMultiProfilePagerAdapter.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 = getPackageManager() +                    .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; +        } + +        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 +                : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED, +                intent.getAction() + ":" + intent.getType() + ":" +                        + (categories != null ? Arrays.toString(categories.toArray()) : "")); +    } + +    protected MultiProfilePagerAdapter createMultiProfilePagerAdapter( +            Intent[] initialIntents, +            List<ResolveInfo> resolutionList, +            boolean filterLastUsed, +            TargetDataLoader targetDataLoader) { +        MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; +        if (shouldShowTabs()) { +            resolverMultiProfilePagerAdapter = +                    createResolverMultiProfilePagerAdapterForTwoProfiles( +                            initialIntents, resolutionList, filterLastUsed, targetDataLoader); +        } else { +            resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile( +                    initialIntents, resolutionList, filterLastUsed, targetDataLoader); +        } +        return resolverMultiProfilePagerAdapter; +    } + +    protected EmptyStateProvider createBlockerEmptyStateProvider() { +        final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser()); + +        if (!shouldShowNoCrossProfileIntentsEmptyState) { +            // Implementation that doesn't show any blockers +            return new EmptyStateProvider() {}; +        } + +        final EmptyState noWorkToPersonalEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this, +                        /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, +                        /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, +                        /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL, +                        /* defaultSubtitleResource= */ +                        R.string.resolver_cant_access_personal_apps_explanation, +                        /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL, +                        /* devicePolicyEventCategory= */ +                                ResolverActivity.METRICS_CATEGORY_RESOLVER); + +        final EmptyState noPersonalToWorkEmptyState = +                new DevicePolicyBlockerEmptyState( +                        /* context= */ this, +                        /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, +                        /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, +                        /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK, +                        /* defaultSubtitleResource= */ +                        R.string.resolver_cant_access_work_apps_explanation, +                        /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK, +                        /* devicePolicyEventCategory= */ +                                ResolverActivity.METRICS_CATEGORY_RESOLVER); + +        return new NoCrossProfileEmptyStateProvider( +                requireAnnotatedUserHandles().personalProfileUserHandle, +                noWorkToPersonalEmptyState, +                noPersonalToWorkEmptyState, +                createCrossProfileIntentsChecker(), +                requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch); +    } + +    /** +     * Numerous layouts are supported, each with optional ViewGroups. +     * Make sure the inset gets added to the correct View, using +     * a footer for Lists so it can properly scroll under the navbar. +     */ +    protected boolean shouldAddFooterView() { +        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; +    } + +    protected void applyFooterView(int height) { +        if (mFooterSpacer == null) { +            mFooterSpacer = new Space(getApplicationContext()); +        } else { +            ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) +                .getActiveAdapterView().removeFooterView(mFooterSpacer); +        } +        mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, +                                                                   mSystemWindowInsets.bottom)); +        ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) +            .getActiveAdapterView().addFooterView(mFooterSpacer); +    } + +    protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { +        mSystemWindowInsets = insets.getSystemWindowInsets(); + +        mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, +                mSystemWindowInsets.right, 0); + +        resetButtonBar(); + +        if (shouldUseMiniResolver()) { +            View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container); +            buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom +                    + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing)); +        } + +        // Need extra padding so the list can fully scroll up +        if (shouldAddFooterView()) { +            applyFooterView(mSystemWindowInsets.bottom); +        } + +        return insets.consumeSystemWindowInsets(); +    } + +    @Override +    public void onConfigurationChanged(Configuration newConfig) { +        super.onConfigurationChanged(newConfig); +        mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); +        if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault() +                && !shouldUseMiniResolver()) { +            updateIntentPickerPaddings(); +        } + +        if (mSystemWindowInsets != null) { +            mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, +                    mSystemWindowInsets.right, 0); +        } +    } + +    public int getLayoutResource() { +        return R.layout.resolver_list; +    } + +    @Override +    protected 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() +                && !mLogic.getResolvingHome() && !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(); +            } +        } +        // TODO: should we clean up the work-profile manager before we potentially finish() above? +        mLogic.getWorkProfileAvailabilityManager().unregisterWorkProfileStateReceiver(this); +    } + +    @Override +    protected void onDestroy() { +        super.onDestroy(); +        if (!isChangingConfigurations() && mPickOptionRequest != null) { +            mPickOptionRequest.cancel(); +        } +        if (mMultiProfilePagerAdapter != null +                && mMultiProfilePagerAdapter.getActiveListAdapter() != null) { +            mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy(); +        } +    } + +    public void onButtonClick(View v) { +        final int id = v.getId(); +        ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); +        ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter(); +        int which = currentListAdapter.hasFilteredItem() +                ? currentListAdapter.getFilteredPosition() +                : listView.getCheckedItemPosition(); +        boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem(); +        startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered); +    } + +    public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) { +        if (isFinishing()) { +            return; +        } +        ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter() +                .resolveInfoForPosition(which, hasIndexBeenFiltered); +        if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) { +            String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString(); +            Toast.makeText(this, +                    mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName), +                    Toast.LENGTH_LONG).show(); +            return; +        } + +        TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter() +                .targetInfoForPosition(which, hasIndexBeenFiltered); +        if (target == null) { +            return; +        } +        if (onTargetSelected(target, always)) { +            if (always && mLogic.getSupportsAlwaysUseOption()) { +                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); +            } +            MetricsLogger.action(this, +                    mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() +                            ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED +                            : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); +            finish(); +        } +    } + +    /** +     * Replace me in subclasses! +     */ +    @Override // ResolverListCommunicator +    public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { +        return defIntent; +    } + +    protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) { +        final ItemClickListener listener = new ItemClickListener(); +        setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener); +        if (shouldShowTabs() && mIsIntentPicker) { +            final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel); +            if (rdl != null) { +                rdl.setMaxCollapsedHeight(getResources() +                        .getDimensionPixelSize(useLayoutWithDefault() +                                ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs +                                : R.dimen.resolver_max_collapsed_height_with_tabs)); +            } +        } +    } + +    protected boolean onTargetSelected(TargetInfo target, boolean always) { +        final ResolveInfo ri = target.getResolveInfo(); +        final Intent intent = target != null ? target.getResolvedIntent() : null; + +        if (intent != null && (mLogic.getSupportsAlwaysUseOption() +                || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()) +                && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) { +            // Build a reasonable intent filter, based on what matched. +            IntentFilter filter = new IntentFilter(); +            Intent filterIntent; + +            if (intent.getSelector() != null) { +                filterIntent = intent.getSelector(); +            } else { +                filterIntent = intent; +            } + +            String action = filterIntent.getAction(); +            if (action != null) { +                filter.addAction(action); +            } +            Set<String> categories = filterIntent.getCategories(); +            if (categories != null) { +                for (String cat : categories) { +                    filter.addCategory(cat); +                } +            } +            filter.addCategory(Intent.CATEGORY_DEFAULT); + +            int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK; +            Uri data = filterIntent.getData(); +            if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { +                String mimeType = filterIntent.resolveType(this); +                if (mimeType != null) { +                    try { +                        filter.addDataType(mimeType); +                    } catch (IntentFilter.MalformedMimeTypeException e) { +                        Log.w("ResolverActivity", e); +                        filter = null; +                    } +                } +            } +            if (data != null && data.getScheme() != null) { +                // We need the data specification if there was no type, +                // OR if the scheme is not one of our magical "file:" +                // or "content:" schemes (see IntentFilter for the reason). +                if (cat != IntentFilter.MATCH_CATEGORY_TYPE +                        || (!"file".equals(data.getScheme()) +                                && !"content".equals(data.getScheme()))) { +                    filter.addDataScheme(data.getScheme()); + +                    // Look through the resolved filter to determine which part +                    // of it matched the original Intent. +                    Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator(); +                    if (pIt != null) { +                        String ssp = data.getSchemeSpecificPart(); +                        while (ssp != null && pIt.hasNext()) { +                            PatternMatcher p = pIt.next(); +                            if (p.match(ssp)) { +                                filter.addDataSchemeSpecificPart(p.getPath(), p.getType()); +                                break; +                            } +                        } +                    } +                    Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator(); +                    if (aIt != null) { +                        while (aIt.hasNext()) { +                            IntentFilter.AuthorityEntry a = aIt.next(); +                            if (a.match(data) >= 0) { +                                int port = a.getPort(); +                                filter.addDataAuthority(a.getHost(), +                                        port >= 0 ? Integer.toString(port) : null); +                                break; +                            } +                        } +                    } +                    pIt = ri.filter.pathsIterator(); +                    if (pIt != null) { +                        String path = data.getPath(); +                        while (path != null && pIt.hasNext()) { +                            PatternMatcher p = pIt.next(); +                            if (p.match(path)) { +                                filter.addDataPath(p.getPath(), p.getType()); +                                break; +                            } +                        } +                    } +                } +            } + +            if (filter != null) { +                final int N = mMultiProfilePagerAdapter.getActiveListAdapter() +                        .getUnfilteredResolveList().size(); +                ComponentName[] set; +                // If we don't add back in the component for forwarding the intent to a managed +                // profile, the preferred activity may not be updated correctly (as the set of +                // components we tell it we knew about will have changed). +                final boolean needToAddBackProfileForwardingComponent = +                        mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null; +                if (!needToAddBackProfileForwardingComponent) { +                    set = new ComponentName[N]; +                } else { +                    set = new ComponentName[N + 1]; +                } + +                int bestMatch = 0; +                for (int i=0; i<N; i++) { +                    ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter() +                            .getUnfilteredResolveList().get(i).getResolveInfoAt(0); +                    set[i] = new ComponentName(r.activityInfo.packageName, +                            r.activityInfo.name); +                    if (r.match > bestMatch) bestMatch = r.match; +                } + +                if (needToAddBackProfileForwardingComponent) { +                    set[N] = mMultiProfilePagerAdapter.getActiveListAdapter() +                            .getOtherProfile().getResolvedComponentName(); +                    final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter() +                            .getOtherProfile().getResolveInfo().match; +                    if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch; +                } + +                if (always) { +                    final int userId = getUserId(); +                    final PackageManager pm = getPackageManager(); + +                    // Set the preferred Activity +                    pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent()); + +                    if (ri.handleAllWebDataURI) { +                        // Set default Browser if needed +                        final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId); +                        if (TextUtils.isEmpty(packageName)) { +                            pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId); +                        } +                    } +                } else { +                    try { +                        mMultiProfilePagerAdapter.getActiveListAdapter() +                                .mResolverListController.setLastChosen(intent, filter, bestMatch); +                    } catch (RemoteException re) { +                        Log.d(TAG, "Error calling setLastChosenActivity\n" + re); +                    } +                } +            } +        } + +        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; +    } + +    public void onActivityStarted(TargetInfo cti) { +        // Do nothing +    } + +    @Override // ResolverListCommunicator +    public boolean shouldGetActivityMetadata() { +        return false; +    } + +    public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { +        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(), +                        null, +                        null, +                        getResolverRankerServiceUserHandleList(userHandle), +                        null); +        return new ResolverListController( +                this, +                mPm, +                mLogic.getTargetIntent(), +                mLogic.getReferrerPackageName(), +                requireAnnotatedUserHandles().userIdOfCallingApp, +                resolverComparator, +                getQueryIntentsUser(userHandle)); +    } + +    /** +     * 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); +    } + +    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() { } + +    /** +     * 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 (shouldShowTabs()) { +                textView.setGravity(Gravity.CENTER); +            } +            stub.addView(textView); +        } +    } + +    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"); +            return; +        } +        ResolverListAdapter activeListAdapter = +                mMultiProfilePagerAdapter.getActiveListAdapter(); +        View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider); +        if (!useLayoutWithDefault()) { +            int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; +            buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(), +                    buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize( +                            R.dimen.resolver_button_bar_spacing) + inset); +        } +        if (activeListAdapter.isTabLoaded() +                && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter) +                && !useLayoutWithDefault()) { +            buttonLayout.setVisibility(View.INVISIBLE); +            if (buttonBarDivider != null) { +                buttonBarDivider.setVisibility(View.INVISIBLE); +            } +            setButtonBarIgnoreOffset(/* ignoreOffset */ false); +            return; +        } +        if (buttonBarDivider != null) { +            buttonBarDivider.setVisibility(View.VISIBLE); +        } +        buttonLayout.setVisibility(View.VISIBLE); +        setButtonBarIgnoreOffset(/* ignoreOffset */ true); + +        mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once); +        mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always); + +        resetAlwaysOrOnceButtonBar(); +    } + +    protected String getMetricsCategory() { +        return METRICS_CATEGORY_RESOLVER; +    } + +    @Override // ResolverListCommunicator +    public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) { +        if (!mMultiProfilePagerAdapter.onHandlePackagesChanged( +                listAdapter, +                mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile())) { +            // We no longer have any items... just finish the activity. +            finish(); +        } +    } + +    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)) { +            mMultiProfilePagerAdapter.rebuildActiveTab(true); +        } else { +            mMultiProfilePagerAdapter.clearInactiveProfileCache(); +        } +        return Unit.INSTANCE; +    } + +    // @NonFinalForTesting +    @VisibleForTesting +    protected ResolverListAdapter createResolverListAdapter( +            Context context, +            List<Intent> payloadIntents, +            Intent[] initialIntents, +            List<ResolveInfo> resolutionList, +            boolean filterLastUsed, +            UserHandle userHandle, +            TargetDataLoader targetDataLoader) { +        UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() +                && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle) +                ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle; +        return new ResolverListAdapter( +                context, +                payloadIntents, +                initialIntents, +                resolutionList, +                filterLastUsed, +                createListController(userHandle), +                userHandle, +                mLogic.getTargetIntent(), +                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; +    } + +    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 +        ); +    } + +    private ResolverMultiProfilePagerAdapter +            createResolverMultiProfilePagerAdapterForOneProfile( +                    Intent[] initialIntents, +                    List<ResolveInfo> resolutionList, +                    boolean filterLastUsed, +                    TargetDataLoader targetDataLoader) { +        ResolverListAdapter adapter = createResolverListAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                initialIntents, +                resolutionList, +                filterLastUsed, +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, +                targetDataLoader); +        return new ResolverMultiProfilePagerAdapter( +                /* context */ this, +                adapter, +                createEmptyStateProvider(/* workProfileUserHandle= */ null), +                /* workProfileQuietModeChecker= */ () -> false, +                /* workProfileUserHandle= */ null, +                requireAnnotatedUserHandles().cloneProfileUserHandle); +    } + +    private UserHandle getIntentUser() { +        return getIntent().hasExtra(EXTRA_CALLING_USER) +                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) +                : requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch; +    } + +    private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( +            Intent[] initialIntents, +            List<ResolveInfo> resolutionList, +            boolean filterLastUsed, +            TargetDataLoader targetDataLoader) { +        // 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. +        int selectedProfile = getCurrentProfile(); +        UserHandle intentUser = getIntentUser(); +        if (!requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) { +            if (requireAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) { +                selectedProfile = PROFILE_PERSONAL; +            } else if (requireAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) { +                selectedProfile = PROFILE_WORK; +            } +        } else { +            int selectedProfileExtra = getSelectedProfileExtra(); +            if (selectedProfileExtra != -1) { +                selectedProfile = selectedProfileExtra; +            } +        } +        // We only show the default app for the profile of the current user. The filterLastUsed +        // flag determines whether to show a default app and that app is not shown in the +        // resolver list. So filterLastUsed should be false for the other profile. +        ResolverListAdapter personalAdapter = createResolverListAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                selectedProfile == PROFILE_PERSONAL ? initialIntents : null, +                resolutionList, +                (filterLastUsed && UserHandle.myUserId() +                        == requireAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()), +                /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle, +                targetDataLoader); +        UserHandle workProfileUserHandle = requireAnnotatedUserHandles().workProfileUserHandle; +        ResolverListAdapter workAdapter = createResolverListAdapter( +                /* context */ this, +                mLogic.getPayloadIntents(), +                selectedProfile == PROFILE_WORK ? initialIntents : null, +                resolutionList, +                (filterLastUsed && UserHandle.myUserId() +                        == workProfileUserHandle.getIdentifier()), +                /* userHandle */ workProfileUserHandle, +                targetDataLoader); +        return new ResolverMultiProfilePagerAdapter( +                /* context */ this, +                personalAdapter, +                workAdapter, +                createEmptyStateProvider(workProfileUserHandle), +                () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(), +                selectedProfile, +                workProfileUserHandle, +                requireAnnotatedUserHandles().cloneProfileUserHandle); +    } + +    /** +     * 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."); +            } +        } +        return selectedProfile; +    } + +    protected final @Profile int getCurrentProfile() { +        UserHandle launchUser = requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch; +        UserHandle personalUser = requireAnnotatedUserHandles().personalProfileUserHandle; +        return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK; +    } + +    private AnnotatedUserHandles requireAnnotatedUserHandles() { +        return requireNonNull(mLogic.getAnnotatedUserHandles()); +    } + +    private boolean hasWorkProfile() { +        return requireAnnotatedUserHandles().workProfileUserHandle != null; +    } + +    private boolean hasCloneProfile() { +        return requireAnnotatedUserHandles().cloneProfileUserHandle != null; +    } + +    protected final boolean isLaunchedAsCloneProfile() { +        UserHandle launchUser = requireAnnotatedUserHandles().userHandleSharesheetLaunchedAs; +        UserHandle cloneUser = requireAnnotatedUserHandles().cloneProfileUserHandle; +        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( +                titleCont.getPaddingLeft(), +                titleCont.getPaddingTop(), +                titleCont.getPaddingRight(), +                getResources().getDimensionPixelSize(R.dimen.resolver_title_padding_bottom)); +        View buttonBar = findViewById(com.android.internal.R.id.button_bar); +        buttonBar.setPadding( +                buttonBar.getPaddingLeft(), +                getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing), +                buttonBar.getPaddingRight(), +                getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing)); +    } + +    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(); +    } + +    @Override // ResolverListCommunicator +    public final void sendVoiceChoicesIfNeeded() { +        if (!isVoiceInteraction()) { +            // Clearly not needed. +            return; +        } + +        int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount(); +        final Option[] options = new Option[count]; +        for (int i = 0; i < options.length; i++) { +            TargetInfo target = mMultiProfilePagerAdapter.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 PickTargetOptionRequest( +                new Prompt(getTitle()), options, null); +        getVoiceInteractor().submitRequest(mPickOptionRequest); +    } + +    final Option optionForChooserTarget(TargetInfo target, int index) { +        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() +                ? ActionTitle.HOME +                : 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 = +                mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0; +        if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { +            return getString(defaultTitleRes); +        } else { +            return named +                    ? getString( +                            title.namedTitleRes, +                            getOrLoadDisplayLabel( +                                    mMultiProfilePagerAdapter +                                        .getActiveListAdapter().getFilteredItem())) +                    : getString(title.titleRes); +        } +    } + +    final void dismiss() { +        if (!isFinishing()) { +            finish(); +        } +    } + +    @Override +    protected final void onRestart() { +        super.onRestart(); +        if (!mRegistered) { +            mPersonalPackageMonitor.register( +                    this, +                    getMainLooper(), +                    requireAnnotatedUserHandles().personalProfileUserHandle, +                    false); +            if (hasWorkProfile()) { +                if (mWorkPackageMonitor == null) { +                    mWorkPackageMonitor = createPackageMonitor( +                            mMultiProfilePagerAdapter.getWorkListAdapter()); +                } +                mWorkPackageMonitor.register( +                        this, +                        getMainLooper(), +                        requireAnnotatedUserHandles().workProfileUserHandle, +                        false); +            } +            mRegistered = true; +        } +        WorkProfileAvailabilityManager workProfileAvailabilityManager = +                mLogic.getWorkProfileAvailabilityManager(); +        if (hasWorkProfile() && workProfileAvailabilityManager.isWaitingToEnableWorkProfile()) { +            if (workProfileAvailabilityManager.isQuietModeEnabled()) { +                workProfileAvailabilityManager.markWorkProfileEnabledBroadcastReceived(); +            } +        } +        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 +    protected final void onSaveInstanceState(@NonNull 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()); +        } +    } + +    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; +    } + +    private boolean supportsManagedProfiles(ResolveInfo resolveInfo) { +        try { +            ApplicationInfo appInfo = getPackageManager().getApplicationInfo( +                    resolveInfo.activityInfo.packageName, 0 /* default flags */); +            return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP; +        } catch (NameNotFoundException e) { +            return false; +        } +    } + +    private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos, +            boolean filtered) { +        if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) { +            // Never allow the inactive profile to always open an app. +            mAlwaysButton.setEnabled(false); +            return; +        } +        // 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)) { +            mAlwaysButton.setEnabled(false); +            return; +        } +        boolean enabled = false; +        ResolveInfo ri = null; +        if (hasValidSelection) { +            ri = mMultiProfilePagerAdapter.getActiveListAdapter() +                    .resolveInfoForPosition(checkedPos, filtered); +            if (ri == null) { +                Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled"); +                return; +            } else if (ri.targetUserId != UserHandle.USER_CURRENT) { +                Log.e(TAG, "Attempted to set selection to resolve info for another user"); +                return; +            } else { +                enabled = true; +            } + +            mAlwaysButton.setText(getResources() +                    .getString(R.string.activity_resolver_use_always)); +        } + +        if (ri != null) { +            ActivityInfo activityInfo = ri.activityInfo; + +            boolean hasRecordPermission = +                    mPm.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); +                enabled = !hasAudioCapture; +            } +        } +        mAlwaysButton.setEnabled(enabled); +    } + +    @Override // ResolverListCommunicator +    public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, +            boolean rebuildCompleted) { +        if (isAutolaunching()) { +            return; +        } +        if (mIsIntentPicker) { +            ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) +                    .setUseLayoutWithDefault(useLayoutWithDefault()); +        } +        if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) { +            mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter); +        } else { +            mMultiProfilePagerAdapter.showListView(listAdapter); +        } +        // showEmptyResolverListEmptyState can mark the tab as loaded, +        // which is a precondition for auto launching +        if (rebuildCompleted && maybeAutolaunchActivity()) { +            return; +        } +        if (doPostProcessing) { +            maybeCreateHeader(listAdapter); +            resetButtonBar(); +            onListRebuilt(listAdapter, rebuildCompleted); +        } +    } + +    /** 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); +    } + +    /** +     * 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 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 = 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)) +                .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); +        startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle()); +    } + +    /** +     * Sets up the content view. +     * @return <code>true</code> if the activity is finishing and creation should halt. +     */ +    private boolean configureContentView(TargetDataLoader targetDataLoader) { +        if (mMultiProfilePagerAdapter.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. +        // To date, we really only care about "partially rebuilding" tabs for work and/or personal. +        boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildTabs(shouldShowTabs()); + +        if (shouldUseMiniResolver()) { +            configureMiniResolverContent(targetDataLoader); +            Trace.endSection(); +            return false; +        } + +        if (useLayoutWithDefault()) { +            mLayoutId = R.layout.resolver_list_with_default; +        } else { +            mLayoutId = getLayoutResource(); +        } +        setContentView(mLayoutId); +        mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager)); +        boolean result = postRebuildList(rebuildCompleted); +        Trace.endSection(); +        return result; +    } + +    /** +     * Mini resolver is shown when the user is choosing between browser[s] in this profile and a +     * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon +     * and asks the user if they'd like to open that cross-profile app or use the in-profile +     * browser. +     */ +    private void configureMiniResolverContent(TargetDataLoader targetDataLoader) { +        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 = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getPersonalListAdapter() +                : mMultiProfilePagerAdapter.getWorkListAdapter(); + +        ResolverListAdapter inactiveAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getWorkListAdapter() +                : mMultiProfilePagerAdapter.getPersonalListAdapter(); + +        DisplayResolveInfo sameProfileResolveInfo = sameProfileAdapter.getFirstDisplayResolveInfo(); + +        final DisplayResolveInfo otherProfileResolveInfo = +                inactiveAdapter.getFirstDisplayResolveInfo(); + +        // Load the icon asynchronously +        ImageView icon = findViewById(com.android.internal.R.id.icon); +        targetDataLoader.loadAppTargetIcon( +                otherProfileResolveInfo, +                inactiveAdapter.getUserHandle(), +                (drawable) -> { +                    if (!isDestroyed()) { +                        otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); +                        new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); +                    } +                }); + +        ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText( +                getResources().getString( +                        inWorkProfile +                                ? R.string.miniresolver_open_in_personal +                                : R.string.miniresolver_open_in_work, +                        getOrLoadDisplayLabel(otherProfileResolveInfo))); +        ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText( +                inWorkProfile ? R.string.miniresolver_use_work_browser +                        : R.string.miniresolver_use_personal_browser); + +        findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener( +                v -> { +                    safelyStartActivity(sameProfileResolveInfo); +                    finish(); +                }); + +        findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> { +            Intent intent = otherProfileResolveInfo.getResolvedIntent(); +            safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle()); +            finish(); +        }); +    } + +    private boolean isTwoPagePersonalAndWorkConfiguration() { +        return (mMultiProfilePagerAdapter.getCount() == 2) +                && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL) +                && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK); +    } + +    /** +     * 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. +     */ +    private boolean shouldUseMiniResolver() { +        if (!mIsIntentPicker) { +            return false; +        } +        if (!isTwoPagePersonalAndWorkConfiguration()) { +            return false; +        } + +        ResolverListAdapter sameProfileAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getPersonalListAdapter() +                : mMultiProfilePagerAdapter.getWorkListAdapter(); + +        ResolverListAdapter otherProfileAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getWorkListAdapter() +                : mMultiProfilePagerAdapter.getPersonalListAdapter(); + +        if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) { +            Log.d(TAG, "No targets in the current profile"); +            return false; +        } + +        if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) { +            Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount()); +            return false; +        } + +        if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) { +            Log.d(TAG, "Other profile is a web browser"); +            return false; +        } + +        if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) { +            Log.d(TAG, "Non-browser found in this profile"); +            return false; +        } + +        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) { +            return false; +        } + +        if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) { +            return false; +        } + +        // Only one target, so we're a candidate to auto-launch! +        final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter() +                .targetInfoForPosition(0, false); +        if (shouldAutoLaunchSingleChoice(target)) { +            safelyStartActivity(target); +            finish(); +            return true; +        } +        return false; +    } + +    /** +     * When we have just 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 = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getPersonalListAdapter() +                : mMultiProfilePagerAdapter.getWorkListAdapter(); + +        ResolverListAdapter inactiveListAdapter = +                (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL) +                ? mMultiProfilePagerAdapter.getWorkListAdapter() +                : mMultiProfilePagerAdapter.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 (!canAppInteractCrossProfiles(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; +    } + +    /** +     * 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> +     * +     */ +    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) { +            return false; +        } + +        int packageUid = applicationInfo.uid; + +        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; +        } +        if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES, +                PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) { +            return true; +        } +        return false; +    } + +    private boolean isAutolaunching() { +        return !mRegistered && isFinishing(); +    } + +    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(); +        }); + +        viewPager.setVisibility(View.VISIBLE); +        tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage()); +        mMultiProfilePagerAdapter.setOnProfileSelectedListener( +                new MultiProfilePagerAdapter.OnProfileSelectedListener() { +                    @Override +                    public void onProfileSelected(int index) { +                        tabHost.setCurrentTab(index); +                        resetButtonBar(); +                        resetCheckedItem(); +                    } + +                    @Override +                    public void onProfilePageStateChanged(int state) { +                        onHorizontalSwipeStateChanged(state); +                    } +                }); +        mOnSwitchOnWorkSelectedListener = () -> { +            final View workTab = tabHost.getTabWidget().getChildAt(1); +            workTab.setFocusable(true); +            workTab.setFocusableInTouchMode(true); +            workTab.requestFocus(); +        }; +    } + +    private void maybeHideDivider() { +        if (!mIsIntentPicker) { +            return; +        } +        final View divider = findViewById(com.android.internal.R.id.divider); +        if (divider == null) { +            return; +        } +        divider.setVisibility(View.GONE); +    } + +    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)) { +            addUseDifferentAppLabelIfNecessary(activeListAdapter); +        } +    } + +    /** +     * Updates the button bar container {@code ignoreOffset} layout param. +     * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of +     * the screen. +     */ +    private void setButtonBarIgnoreOffset(boolean ignoreOffset) { +        View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container); +        if (buttonBarContainer != null) { +            ResolverDrawerLayout.LayoutParams layoutParams = +                    (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams(); +            layoutParams.ignoreOffset = ignoreOffset; +            buttonBarContainer.setLayoutParams(layoutParams); +        } +    } + +    private void setupAdapterListView(ListView listView, ItemClickListener listener) { +        listView.setOnItemClickListener(listener); +        listView.setOnItemLongClickListener(listener); + +        if (mLogic.getSupportsAlwaysUseOption()) { +            listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); +        } +    } + +    /** +     * 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 (!shouldShowTabs() +                && 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 = mLogic.getTitle() != null +                ? mLogic.getTitle() +                : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId()); + +        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(); +    } + +    private void resetAlwaysOrOnceButtonBar() { +        // Disable both buttons initially +        setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false); +        mOnceButton.setEnabled(false); + +        int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter() +                .getFilteredPosition(); +        if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) { +            setAlwaysButtonEnabled(true, filteredPosition, false); +            mOnceButton.setEnabled(true); +            // Focus the button if we already have the default option +            mOnceButton.requestFocus(); +            return; +        } + +        // When the items load in, if an item was already selected, enable the buttons +        ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); +        if (currentAdapterView != null +                && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) { +            setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true); +            mOnceButton.setEnabled(true); +        } +    } + +    @Override // ResolverListCommunicator +    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; +    } + +    final class ItemClickListener implements AdapterView.OnItemClickListener, +            AdapterView.OnItemLongClickListener { +        @Override +        public void onItemClick(AdapterView<?> parent, View view, int position, long id) { +            final ListView listView = parent instanceof ListView ? (ListView) parent : null; +            if (listView != null) { +                position -= listView.getHeaderViewsCount(); +            } +            if (position < 0) { +                // Header views don't count. +                return; +            } +            // If we're still loading, we can't yet enable the buttons. +            if (mMultiProfilePagerAdapter.getActiveListAdapter() +                    .resolveInfoForPosition(position, true) == null) { +                return; +            } +            ListView currentAdapterView = +                    (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); +            final int checkedPos = currentAdapterView.getCheckedItemPosition(); +            final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; +            if (!useLayoutWithDefault() +                    && (!hasValidSelection || mLastSelected != checkedPos) +                    && mAlwaysButton != null) { +                setAlwaysButtonEnabled(hasValidSelection, checkedPos, true); +                mOnceButton.setEnabled(hasValidSelection); +                if (hasValidSelection) { +                    currentAdapterView.smoothScrollToPosition(checkedPos); +                    mOnceButton.requestFocus(); +                } +                mLastSelected = checkedPos; +            } else { +                startSelected(position, false, true); +            } +        } + +        @Override +        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { +            final ListView listView = parent instanceof ListView ? (ListView) parent : null; +            if (listView != null) { +                position -= listView.getHeaderViewsCount(); +            } +            if (position < 0) { +                // Header views don't count. +                return false; +            } +            ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter() +                    .resolveInfoForPosition(position, true); +            showTargetDetails(ri); +            return true; +        } + +    } + +    /** 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; +    } + +    static final class PickTargetOptionRequest extends PickOptionRequest { +        public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options, +                @Nullable Bundle extras) { +            super(prompt, options, extras); +        } + +        @Override +        public void onCancel() { +            super.onCancel(); +            final ResolverActivity ra = (ResolverActivity) getActivity(); +            if (ra != null) { +                ra.mPickOptionRequest = null; +                ra.finish(); +            } +        } + +        @Override +        public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) { +            super.onPickOptionResult(finished, selections, result); +            if (selections.length != 1) { +                // TODO In a better world we would filter the UI presented here and let the +                // user refine. Maybe later. +                return; +            } + +            final ResolverActivity ra = (ResolverActivity) getActivity(); +            if (ra != null) { +                final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter() +                        .getItem(selections[0].getIndex()); +                if (ra.onTargetSelected(ti, false)) { +                    ra.mPickOptionRequest = null; +                    ra.finish(); +                } +            } +        } +    } +    /** +     * 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); +    } + +    /** +     * 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; +    } + +    private CharSequence getOrLoadDisplayLabel(TargetInfo info) { +        if (info.isDisplayResolveInfo()) { +            mLogic.getTargetDataLoader().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 new file mode 100644 index 00000000..0e2b25ec --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt @@ -0,0 +1,81 @@ +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, +    onWorkProfileStatusUpdated: () -> Unit, +) : +    ActivityLogic, +    CommonActivityLogic by CommonActivityLogicImpl( +        tag, +        activityProvider, +        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/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java new file mode 100644 index 00000000..d96fd15a --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2; + +import android.content.Context; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ListView; + +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; + +import java.util.Optional; +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, +                                            EmptyStateProvider emptyStateProvider, +                                            Supplier<Boolean> workProfileQuietModeChecker, +                                            @Profile int defaultProfile, +                                            UserHandle workProfileUserHandle, +                                            UserHandle cloneProfileUserHandle) { +        this( +                context, +                ImmutableList.of(personalAdapter, workAdapter), +                emptyStateProvider, +                workProfileQuietModeChecker, +                defaultProfile, +                workProfileUserHandle, +                cloneProfileUserHandle, +                new BottomPaddingOverrideSupplier()); +    } + +    private ResolverMultiProfilePagerAdapter( +            Context context, +            ImmutableList<ResolverListAdapter> listAdapters, +            EmptyStateProvider emptyStateProvider, +            Supplier<Boolean> workProfileQuietModeChecker, +            @Profile int defaultProfile, +            UserHandle workProfileUserHandle, +            UserHandle cloneProfileUserHandle, +            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { +        super( +                        listAdapter -> listAdapter, +                        (listView, bindAdapter) -> listView.setAdapter(bindAdapter), +                listAdapters, +                emptyStateProvider, +                workProfileQuietModeChecker, +                defaultProfile, +                workProfileUserHandle, +                cloneProfileUserHandle, +                        () -> (ViewGroup) LayoutInflater.from(context).inflate( +                                R.layout.resolver_list_per_profile, null, false), +                bottomPaddingOverrideSupplier); +        mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; +    } + +    public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { +        mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault); +    } + +    /** 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); +        } +    } + +    private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> { +        private boolean mUseLayoutWithDefault; + +        public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { +            mUseLayoutWithDefault = useLayoutWithDefault; +        } + +        @Override +        public Optional<Integer> get() { +            return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt new file mode 100644 index 00000000..1a58afcb --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt @@ -0,0 +1,46 @@ +package com.android.intentresolver.v2.data + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import android.util.Log +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +private const val TAG = "BroadcastFlow" + +/** + * Returns a [callbackFlow] that, when collected, registers a broadcast receiver and emits a new + * value whenever broadcast matching _filter_ is received. The result value will be computed using + * [transform] and emitted if non-null. + */ +internal fun <T> broadcastFlow( +    context: Context, +    filter: IntentFilter, +    user: UserHandle, +    transform: (Intent) -> T? +): Flow<T> = callbackFlow { +    val receiver = +        object : BroadcastReceiver() { +            override fun onReceive(context: Context, intent: Intent) { +                transform(intent)?.also { result -> +                    trySend(result).onFailure { Log.e(TAG, "Failed to send $result", it) } +                } +                    ?: Log.w(TAG, "Ignored broadcast $intent") +            } +        } + +    context.registerReceiverAsUser( +        receiver, +        user, +        IntentFilter(filter), +        null, +        null, +        Context.RECEIVER_NOT_EXPORTED +    ) +    awaitClose { context.unregisterReceiver(receiver) } +} diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/data/model/User.kt new file mode 100644 index 00000000..504b04c8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/model/User.kt @@ -0,0 +1,50 @@ +package com.android.intentresolver.v2.data.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 + +/** + * A User represents the owner of a distinct set of content. + * * maps 1:1 to a UserHandle or UserId (Int) value. + * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the + *   [type] property. + * + * See + * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md) + * + * ``` + * val users = listOf( + *     User(id = 0, role = PERSONAL), + *     User(id = 10, role = WORK), + *     User(id = 11, role = CLONE), + *     User(id = 12, role = PRIVATE), + * ) + * ``` + */ +data class User( +    @UserIdInt val id: Int, +    val role: Role, +) { +    val handle: UserHandle = UserHandle.of(id) + +    val type: Type +        get() = role.type + +    enum class Type { +        FULL, +        PROFILE +    } + +    enum class Role( +        /** The type of the role user. */ +        val type: Type +    ) { +        PERSONAL(FULL), +        PRIVATE(PROFILE), +        WORK(PROFILE), +        CLONE(PROFILE) +    } +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt new file mode 100644 index 00000000..7debdf07 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt @@ -0,0 +1,68 @@ +/* + * 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.data.repository + +import android.app.admin.DevicePolicyManager +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 +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY +import android.content.res.Resources +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +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) +        }) +    } + +    val workTabLabel by lazy { +        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) +        }) +    } + +    val workTabAccessibilityLabel by lazy { +        requireNotNull(policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) { +            resources.getString(R.string.resolver_work_tab_accessibility) +        }) +    } + +    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)) +    } +}
\ 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 new file mode 100644 index 00000000..fc82efee --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt @@ -0,0 +1,29 @@ +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 + +/** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */ +fun UserInfo.getSupportedUserRole(): Role? = +    when { +        isFull -> Role.PERSONAL +        isManagedProfile -> Role.WORK +        isCloneProfile -> Role.CLONE +        isPrivateProfile -> Role.PRIVATE +        else -> null +    } + +/** + * Creates a [User], based on values from a [UserInfo]. + * + * ``` + * val users: List<User> = + *     getEnabledProfiles(user).map(::toUser).filterNotNull() + * ``` + * + * @return a [User] if the [UserInfo] matched a supported [Role], otherwise null + */ +fun UserInfo.toUser(): User? { +    return getSupportedUserRole()?.let { role -> User(userHandle.identifier, 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 new file mode 100644 index 00000000..dc809b46 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt @@ -0,0 +1,261 @@ +package com.android.intentresolver.v2.data.repository + +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE +import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE +import android.content.Intent.ACTION_PROFILE_ADDED +import android.content.Intent.ACTION_PROFILE_AVAILABLE +import android.content.Intent.ACTION_PROFILE_REMOVED +import android.content.Intent.ACTION_PROFILE_UNAVAILABLE +import android.content.Intent.EXTRA_QUIET_MODE +import android.content.Intent.EXTRA_USER +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import androidx.annotation.VisibleForTesting +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 dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +interface UserRepository { +    /** +     * A [Flow] user profile groups. Each map 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>> + +    /** +     * 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> + +    /** +     * Request that availability be updated to the requested state. This currently includes toggling +     * quiet mode as needed. This may involve additional background actions, such as starting or +     * stopping a profile user (along with their many associated processes). +     * +     * If successful, the change will be applied after the call returns and can be observed using +     * [UserRepository.isAvailable] for the given user. +     * +     * No actions are taken if the user is already in requested state. +     * +     * @throws IllegalArgumentException if called for an unsupported user type +     */ +    suspend fun requestState(user: User, available: Boolean) +} + +private const val TAG = "UserRepository" + +private data class UserWithState(val user: User, val available: Boolean) + +private typealias UserStateMap = Map<UserHandle, UserWithState> + +/** Tracks and publishes state for the parent user and associated profiles. */ +class UserRepositoryImpl +@VisibleForTesting +constructor( +    private val profileParent: UserHandle, +    private val userManager: UserManager, +    /** A flow of events which represent user-state changes from [UserManager]. */ +    private val userEvents: Flow<UserEvent>, +    scope: CoroutineScope, +    private val backgroundDispatcher: CoroutineDispatcher +) : UserRepository { +    @Inject +    constructor( +        @ApplicationContext context: Context, +        @ProfileParent profileParent: UserHandle, +        userManager: UserManager, +        @Main scope: CoroutineScope, +        @Background background: CoroutineDispatcher +    ) : this( +        profileParent, +        userManager, +        userEvents = userBroadcastFlow(context, profileParent), +        scope, +        background +    ) + +    data class UserEvent(val action: String, val user: UserHandle, val quietMode: Boolean = false) + +    /** +     * An exception which indicates that an inconsistency exists between the user state map and the +     * rest of the system. +     */ +    internal class UserStateException( +        override val message: String, +        val event: UserEvent, +        override val cause: Throwable? = null +    ) : RuntimeException("$message: event=$event", cause) + +    private val usersWithState: Flow<UserStateMap> = +        userEvents +            .onStart { emit(UserEvent(INITIALIZE, profileParent)) } +            .onEach { Log.i("UserDataSource", "userEvent: $it") } +            .runningFold<UserEvent, UserStateMap>(emptyMap()) { users, event -> +                try { +                    // Handle an action by performing some operation, then returning a new map +                    when (event.action) { +                        INITIALIZE -> createNewUserStateMap(profileParent) +                        ACTION_PROFILE_ADDED -> handleProfileAdded(event, users) +                        ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users) +                        ACTION_MANAGED_PROFILE_UNAVAILABLE, +                        ACTION_MANAGED_PROFILE_AVAILABLE, +                        ACTION_PROFILE_AVAILABLE, +                        ACTION_PROFILE_UNAVAILABLE -> handleAvailability(event, users) +                        else -> { +                            Log.w(TAG, "Unhandled event: $event)") +                            users +                        } +                    } +                } catch (e: UserStateException) { +                    Log.e(TAG, "An error occurred handling an event: ${e.event}", e) +                    Log.e(TAG, "Attempting to recover...") +                    createNewUserStateMap(profileParent) +                } +            } +            .onEach { Log.i("UserDataSource", "userStateMap: $it") } +            .stateIn(scope, SharingStarted.Eagerly, emptyMap()) +            .filterNot { it.isEmpty() } + +    override val users: Flow<Map<UserHandle, User>> = +        usersWithState.map { map -> map.mapValues { it.value.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 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) +        } +    } + +    private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap { +        val userEntry = +            current[event.user] +                ?: throw UserStateException("User was not present in the map", event) +        return current + (event.user to userEntry.copy(available = !event.quietMode)) +    } + +    private fun handleProfileRemoved(event: UserEvent, current: UserStateMap): UserStateMap { +        if (!current.containsKey(event.user)) { +            throw UserStateException("User was not present in the map", event) +        } +        return current.filterKeys { it != event.user } +    } + +    private suspend fun handleProfileAdded(event: UserEvent, current: UserStateMap): UserStateMap { +        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)) +    } + +    private suspend fun createNewUserStateMap(user: UserHandle): UserStateMap { +        val profiles = readProfileGroup(user) +        return profiles +            .mapNotNull { userInfo -> +                userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) } +            } +            .associateBy { it.user.handle } +    } + +    private suspend fun readProfileGroup(handle: UserHandle): List<UserInfo> { +        return withContext(backgroundDispatcher) { +                @Suppress("DEPRECATION") userManager.getEnabledProfiles(handle.identifier) +            } +            .toList() +    } + +    /** Read [UserInfo] from [UserManager], or null if not found or an unsupported type. */ +    private suspend fun readUser(user: UserHandle): User? { +        val userInfo = +            withContext(backgroundDispatcher) { userManager.getUserInfo(user.identifier) } +        return userInfo?.let { info -> +            info.getSupportedUserRole()?.let { role -> User(info.id, role) } +        } +    } +} + +/** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */ +private fun Intent.toUserEvent(): UserEvent? { +    val action = action +    val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java) +    val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) ?: false +    return if (user == null || action == null) { +        null +    } else { +        UserEvent(action, user, quietMode) +    } +} + +const val INITIALIZE = "INITIALIZE" + +private fun createFilter(actions: Iterable<String>): IntentFilter { +    return IntentFilter().apply { actions.forEach(::addAction) } +} + +private fun UserInfo?.isAvailable(): Boolean { +    return this?.isQuietModeEnabled != true +} + +private fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> { +    val userActions = +        setOf( +            ACTION_PROFILE_ADDED, +            ACTION_PROFILE_REMOVED, + +            // Quiet mode enabled/disabled for managed +            // From: UserController.broadcastProfileAvailabilityChanges +            // In response to setQuietModeEnabled +            ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only +            ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only + +            // Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile +            // true' +            ACTION_PROFILE_AVAILABLE, // quiet mode, +            ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type +        ) +    return broadcastFlow(context, createFilter(userActions), profileParent, Intent::toUserEvent) +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt new file mode 100644 index 00000000..94f985e7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt @@ -0,0 +1,34 @@ +package com.android.intentresolver.v2.data.repository + +import android.content.Context +import android.os.UserHandle +import android.os.UserManager +import com.android.intentresolver.inject.ApplicationUser +import com.android.intentresolver.inject.ProfileParent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface UserRepositoryModule { +    companion object { +        @Provides +        @Singleton +        @ApplicationUser +        fun applicationUser(@ApplicationContext context: Context): UserHandle = context.user + +        @Provides +        @Singleton +        @ProfileParent +        fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle { +            return userManager.getProfileParent(user) ?: user +        } +    } + +    @Binds @Singleton fun userRepository(impl: UserRepositoryImpl): UserRepository +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt new file mode 100644 index 00000000..7ee78d91 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt @@ -0,0 +1,46 @@ +package com.android.intentresolver.v2.data.repository + +import android.content.Context +import androidx.core.content.getSystemService +import com.android.intentresolver.v2.data.model.User + +/** + * Provides cached instances of a [system service][Context.getSystemService] created with + * [the context of a specified user][Context.createContextAsUser]. + * + * System services which have only `@UserHandleAware` APIs operate on the user id available from + * [Context.getUser], the context used to retrieve the service. This utility helps adapt a per-user + * API model to work in multi-user manner. + * + * Example usage: + * ``` + * val usageStats = userScopedService<UsageStatsManager>(context) + * + * fun getStatsForUser( + *     user: User, + *     from: Long, + *     to: Long + * ): UsageStats { + *     return usageStats.forUser(user) + *        .queryUsageStats(INTERVAL_BEST, from, to) + * } + * ``` + */ +interface UserScopedService<T> { +    fun forUser(user: User): T +} + +inline fun <reified T> userScopedService(context: Context): UserScopedService<T> { +    return object : UserScopedService<T> { +        private val map = mutableMapOf<User, T>() + +        override fun forUser(user: User): T { +            return synchronized(this) { +                map.getOrPut(user) { +                    val userContext = context.createContextAsUser(user.handle, 0) +                    requireNotNull(userContext.getSystemService()) +                } +            } +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java new file mode 100644 index 00000000..2f1e1b59 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java @@ -0,0 +1,141 @@ +/* + * 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.emptystate; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.android.intentresolver.emptystate.EmptyState; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by + * some empty-state status. + */ +public class EmptyStateUiHelper { +    private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier; +    private final View mEmptyStateView; +    private final View mListView; +    private final View mEmptyStateContainerView; +    private final TextView mEmptyStateTitleView; +    private final TextView mEmptyStateSubtitleView; +    private final Button mEmptyStateButtonView; +    private final View mEmptyStateProgressView; +    private final View mEmptyStateEmptyView; + +    public EmptyStateUiHelper( +            ViewGroup rootView, +            int listViewResourceId, +            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) { +        mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; +        mEmptyStateView = +                rootView.requireViewById(com.android.internal.R.id.resolver_empty_state); +        mListView = rootView.requireViewById(listViewResourceId); +        mEmptyStateContainerView = mEmptyStateView.requireViewById( +                com.android.internal.R.id.resolver_empty_state_container); +        mEmptyStateTitleView = mEmptyStateView.requireViewById( +                com.android.internal.R.id.resolver_empty_state_title); +        mEmptyStateSubtitleView = mEmptyStateView.requireViewById( +                com.android.internal.R.id.resolver_empty_state_subtitle); +        mEmptyStateButtonView = mEmptyStateView.requireViewById( +                com.android.internal.R.id.resolver_empty_state_button); +        mEmptyStateProgressView = mEmptyStateView.requireViewById( +                com.android.internal.R.id.resolver_empty_state_progress); +        mEmptyStateEmptyView = mEmptyStateView.requireViewById(com.android.internal.R.id.empty); +    } + +    /** +     * Display the described empty state. +     * @param emptyState the data describing the cause of this empty-state condition. +     * @param buttonOnClick handler for a button that the user might be able to use to circumvent +     * the empty-state condition. If null, no button will be displayed. +     */ +    public void showEmptyState(EmptyState emptyState, View.OnClickListener buttonOnClick) { +        resetViewVisibilities(); +        setupContainerPadding(); + +        String title = emptyState.getTitle(); +        if (title != null) { +            mEmptyStateTitleView.setVisibility(View.VISIBLE); +            mEmptyStateTitleView.setText(title); +        } else { +            mEmptyStateTitleView.setVisibility(View.GONE); +        } + +        String subtitle = emptyState.getSubtitle(); +        if (subtitle != null) { +            mEmptyStateSubtitleView.setVisibility(View.VISIBLE); +            mEmptyStateSubtitleView.setText(subtitle); +        } else { +            mEmptyStateSubtitleView.setVisibility(View.GONE); +        } + +        mEmptyStateEmptyView.setVisibility( +                emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE); +        // TODO: The EmptyState API says that if `useDefaultEmptyView()` is true, we'll ignore the +        // state's specified title/subtitle; where (if anywhere) is that implemented? + +        mEmptyStateButtonView.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE); +        mEmptyStateButtonView.setOnClickListener(buttonOnClick); + +        // Don't show the main list view when we're showing an empty state. +        mListView.setVisibility(View.GONE); +    } + +    /** Sets up the padding of the view containing the empty state screens. */ +    public void setupContainerPadding() { +        Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); +        bottomPaddingOverride.ifPresent(paddingBottom -> +                mEmptyStateContainerView.setPadding( +                    mEmptyStateContainerView.getPaddingLeft(), +                    mEmptyStateContainerView.getPaddingTop(), +                    mEmptyStateContainerView.getPaddingRight(), +                    paddingBottom)); +    } + +    public void showSpinner() { +        mEmptyStateTitleView.setVisibility(View.INVISIBLE); +        // TODO: subtitle? +        mEmptyStateButtonView.setVisibility(View.INVISIBLE); +        mEmptyStateProgressView.setVisibility(View.VISIBLE); +        mEmptyStateEmptyView.setVisibility(View.GONE); +    } + +    public void hide() { +        mEmptyStateView.setVisibility(View.GONE); +        mListView.setVisibility(View.VISIBLE); +    } + +    // TODO: this is exposed for testing so we can thoroughly prepare initial conditions that let us +    // observe the resulting change. In reality it's only invoked as part of `showEmptyState()` and +    // we could consider setting up narrower "realistic" preconditions to make assertions about the +    // higher-level operation. +    @VisibleForTesting +    void resetViewVisibilities() { +        mEmptyStateTitleView.setVisibility(View.VISIBLE); +        mEmptyStateSubtitleView.setVisibility(View.VISIBLE); +        mEmptyStateButtonView.setVisibility(View.INVISIBLE); +        mEmptyStateProgressView.setVisibility(View.GONE); +        mEmptyStateEmptyView.setVisibility(View.GONE); +        mEmptyStateView.setVisibility(View.VISIBLE); +    } +} + diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java new file mode 100644 index 00000000..e9d1bb34 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -0,0 +1,157 @@ +/* + * 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_NO_PERSONAL_APPS; +import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS; + +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; +import android.stats.devicepolicy.nano.DevicePolicyEnums; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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; + +/** + * Chooser/ResolverActivity empty state provider that returns empty state which is shown when + * there are no apps available. + */ +public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { + +    @NonNull +    private final Context mContext; +    @Nullable +    private final UserHandle mWorkProfileUserHandle; +    @Nullable +    private final UserHandle mPersonalProfileUserHandle; +    @NonNull +    private final String mMetricsCategory; +    @NonNull +    private final UserHandle mTabOwnerUserHandleForLaunch; + +    public NoAppsAvailableEmptyStateProvider(@NonNull Context context, +            @Nullable UserHandle workProfileUserHandle, +            @Nullable UserHandle personalProfileUserHandle, @NonNull String metricsCategory, +            @NonNull UserHandle tabOwnerUserHandleForLaunch) { +        mContext = context; +        mWorkProfileUserHandle = workProfileUserHandle; +        mPersonalProfileUserHandle = personalProfileUserHandle; +        mMetricsCategory = metricsCategory; +        mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; +    } + +    @Nullable +    @Override +    @SuppressWarnings("ReferenceEquality") +    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { +        UserHandle listUserHandle = resolverListAdapter.getUserHandle(); + +        if (mWorkProfileUserHandle != null +                && (mTabOwnerUserHandleForLaunch.equals(listUserHandle) +                || !hasAppsInOtherProfile(resolverListAdapter))) { + +            String title; +            if (listUserHandle == mPersonalProfileUserHandle) { +                title = mContext.getSystemService( +                        DevicePolicyManager.class).getResources().getString( +                        RESOLVER_NO_PERSONAL_APPS, +                            () -> mContext.getString(R.string.resolver_no_personal_apps_available)); +            } else { +                title = mContext.getSystemService( +                        DevicePolicyManager.class).getResources().getString( +                        RESOLVER_NO_WORK_APPS, +                            () -> mContext.getString(R.string.resolver_no_work_apps_available)); +            } + +            return new NoAppsAvailableEmptyState( +                    title, mMetricsCategory, +                    /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle +            ); +        } else if (mWorkProfileUserHandle == null) { +            // Return default empty state without tracking +            return new DefaultEmptyState(); +        } + +        return null; +    } + +    private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) { +        if (mWorkProfileUserHandle == null) { +            return false; +        } +        List<ResolvedComponentInfo> resolversForIntent = +                adapter.getResolversForUser(mTabOwnerUserHandleForLaunch); +        for (ResolvedComponentInfo info : resolversForIntent) { +            ResolveInfo resolveInfo = info.getResolveInfoAt(0); +            if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { +                return true; +            } +        } +        return false; +    } + +    public static class DefaultEmptyState implements EmptyState { +        @Override +        public boolean useDefaultEmptyView() { +            return true; +        } +    } + +    public static class NoAppsAvailableEmptyState implements EmptyState { + +        @NonNull +        private final String mTitle; + +        @NonNull +        private final String mMetricsCategory; + +        private final boolean mIsPersonalProfile; + +        public NoAppsAvailableEmptyState(@NonNull String title, @NonNull String metricsCategory, +                boolean isPersonalProfile) { +            mTitle = title; +            mMetricsCategory = metricsCategory; +            mIsPersonalProfile = isPersonalProfile; +        } + +        @NonNull +        @Override +        public String getTitle() { +            return mTitle; +        } + +        @Override +        public void onEmptyStateShown() { +            DevicePolicyEventLogger.createEvent( +                            DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED) +                    .setStrings(mMetricsCategory) +                    .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile) +                    .write(); +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java new file mode 100644 index 00000000..b744c589 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.emptystate; + +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; + +/** + * Empty state provider that does not allow cross profile sharing, it will return a blocker + * in case if the profile of the current tab is not the same as the profile of the calling app. + */ +public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { + +    private final UserHandle mPersonalProfileUserHandle; +    private final EmptyState mNoWorkToPersonalEmptyState; +    private final EmptyState mNoPersonalToWorkEmptyState; +    private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; +    private final UserHandle mTabOwnerUserHandleForLaunch; + +    public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle, +            EmptyState noWorkToPersonalEmptyState, +            EmptyState noPersonalToWorkEmptyState, +            CrossProfileIntentsChecker crossProfileIntentsChecker, +            UserHandle tabOwnerUserHandleForLaunch) { +        mPersonalProfileUserHandle = personalUserHandle; +        mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; +        mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; +        mCrossProfileIntentsChecker = crossProfileIntentsChecker; +        mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; +    } + +    @Nullable +    @Override +    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { +        boolean shouldShowBlocker = +                !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle()) +                        && !mCrossProfileIntentsChecker +                        .hasCrossProfileIntents(resolverListAdapter.getIntents(), +                                mTabOwnerUserHandleForLaunch.getIdentifier(), +                                resolverListAdapter.getUserHandle().getIdentifier()); + +        if (!shouldShowBlocker) { +            return null; +        } + +        if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) { +            return mNoWorkToPersonalEmptyState; +        } else { +            return mNoPersonalToWorkEmptyState; +        } +    } + + +    /** +     * Empty state that gets strings from the device policy manager and tracks events into +     * event logger of the device policy events. +     */ +    public static class DevicePolicyBlockerEmptyState implements EmptyState { + +        @NonNull +        private final Context mContext; +        private final String mDevicePolicyStringTitleId; +        @StringRes +        private final int mDefaultTitleResource; +        private final String mDevicePolicyStringSubtitleId; +        @StringRes +        private final int mDefaultSubtitleResource; +        private final int mEventId; +        @NonNull +        private final String mEventCategory; + +        public DevicePolicyBlockerEmptyState(@NonNull Context context, +                String devicePolicyStringTitleId, @StringRes int defaultTitleResource, +                String devicePolicyStringSubtitleId, @StringRes int defaultSubtitleResource, +                int devicePolicyEventId, @NonNull String devicePolicyEventCategory) { +            mContext = context; +            mDevicePolicyStringTitleId = devicePolicyStringTitleId; +            mDefaultTitleResource = defaultTitleResource; +            mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId; +            mDefaultSubtitleResource = defaultSubtitleResource; +            mEventId = devicePolicyEventId; +            mEventCategory = devicePolicyEventCategory; +        } + +        @Nullable +        @Override +        public String getTitle() { +            return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( +                    mDevicePolicyStringTitleId, +                    () -> mContext.getString(mDefaultTitleResource)); +        } + +        @Nullable +        @Override +        public String getSubtitle() { +            return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( +                    mDevicePolicyStringSubtitleId, +                    () -> mContext.getString(mDefaultSubtitleResource)); +        } + +        @Override +        public void onEmptyStateShown() { +            DevicePolicyEventLogger.createEvent(mEventId) +                    .setStrings(mEventCategory) +                    .write(); +        } + +        @Override +        public boolean shouldSkipDataRebuild() { +            return true; +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java new file mode 100644 index 00000000..a6fee3ec --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.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; + +/** + * Chooser/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 WorkProfilePausedEmptyStateProvider implements EmptyStateProvider { + +    private final UserHandle mWorkProfileUserHandle; +    private final WorkProfileAvailabilityManager mWorkProfileAvailability; +    private final String mMetricsCategory; +    private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; +    private final Context mContext; + +    public WorkProfilePausedEmptyStateProvider(@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/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt new file mode 100644 index 00000000..4e8783f8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt @@ -0,0 +1,40 @@ +/* + * 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.icons + +import android.content.Context +import androidx.lifecycle.Lifecycle +import com.android.intentresolver.icons.DefaultTargetDataLoader +import com.android.intentresolver.icons.TargetDataLoader +import com.android.intentresolver.inject.ActivityOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object TargetDataLoaderModule { +    @Provides +    @ActivityScoped +    fun targetDataLoader( +        @ActivityContext context: Context, +        @ActivityOwned lifecycle: Lifecycle, +    ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false) +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt new file mode 100644 index 00000000..5855e2fc --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt @@ -0,0 +1,39 @@ +/* + * 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.listcontroller + +import android.content.ComponentName +import com.android.intentresolver.ChooserRequestParameters + +/** A class that is able to identify components that should be hidden from the user. */ +interface FilterableComponents { +    /** Whether this component should hidden from the user. */ +    fun isComponentFiltered(name: ComponentName): Boolean +} + +/** A class that never filters components. */ +class NoComponentFiltering : FilterableComponents { +    override fun isComponentFiltered(name: ComponentName): Boolean = false +} + +/** A class that filters components by chooser request filter. */ +class ChooserRequestFilteredComponents( +    private val chooserRequestParameters: ChooserRequestParameters, +) : FilterableComponents { +    override fun isComponentFiltered(name: ComponentName): Boolean = +        chooserRequestParameters.filteredComponentNames.contains(name) +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt new file mode 100644 index 00000000..bb9394b4 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt @@ -0,0 +1,70 @@ +package com.android.intentresolver.v2.listcontroller + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.UserHandle +import com.android.intentresolver.ResolvedComponentInfo + +/** A class for translating [Intent]s to [ResolvedComponentInfo]s. */ +interface IntentResolver { +    /** +     * Get data about all the ways the user with the specified handle can resolve any of the +     * provided `intents`. +     */ +    fun getResolversForIntentAsUser( +        shouldGetResolvedFilter: Boolean, +        shouldGetActivityMetadata: Boolean, +        shouldGetOnlyDefaultActivities: Boolean, +        intents: List<Intent>, +        userHandle: UserHandle, +    ): List<ResolvedComponentInfo> +} + +/** Resolves [Intent]s using the [packageManager], deduping using the given [ResolveListDeduper]. */ +class IntentResolverImpl( +    private val packageManager: PackageManager, +    resolveListDeduper: ResolveListDeduper, +) : IntentResolver, ResolveListDeduper by resolveListDeduper { +    override fun getResolversForIntentAsUser( +        shouldGetResolvedFilter: Boolean, +        shouldGetActivityMetadata: Boolean, +        shouldGetOnlyDefaultActivities: Boolean, +        intents: List<Intent>, +        userHandle: UserHandle, +    ): List<ResolvedComponentInfo> { +        val baseFlags = +            ((if (shouldGetOnlyDefaultActivities) PackageManager.MATCH_DEFAULT_ONLY else 0) or +                PackageManager.MATCH_DIRECT_BOOT_AWARE or +                PackageManager.MATCH_DIRECT_BOOT_UNAWARE or +                (if (shouldGetResolvedFilter) PackageManager.GET_RESOLVED_FILTER else 0) or +                (if (shouldGetActivityMetadata) PackageManager.GET_META_DATA else 0) or +                PackageManager.MATCH_CLONE_PROFILE) +        return getResolversForIntentAsUserInternal( +            intents, +            userHandle, +            baseFlags, +        ) +    } + +    private fun getResolversForIntentAsUserInternal( +        intents: List<Intent>, +        userHandle: UserHandle, +        baseFlags: Int, +    ): List<ResolvedComponentInfo> = buildList { +        for (intent in intents) { +            var flags = baseFlags +            if (intent.isWebIntent || intent.flags and Intent.FLAG_ACTIVITY_MATCH_EXTERNAL != 0) { +                flags = flags or PackageManager.MATCH_INSTANT +            } +            // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent. +            val fixedIntent = +                if (intent.javaClass != Intent::class.java) { +                    Intent(intent) +                } else { +                    intent +                } +            val infos = packageManager.queryIntentActivitiesAsUser(fixedIntent, flags, userHandle) +            addToResolveListWithDedupe(this, fixedIntent, infos) +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt new file mode 100644 index 00000000..b2856526 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt @@ -0,0 +1,77 @@ +/* + * 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.listcontroller + +import android.app.AppGlobals +import android.content.ContentResolver +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.IPackageManager +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.RemoteException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** Class that stores and retrieves the most recently chosen resolutions. */ +interface LastChosenManager { + +    /** Returns the most recently chosen resolution. */ +    suspend fun getLastChosen(): ResolveInfo + +    /** Sets the most recently chosen resolution. */ +    suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int) +} + +/** + * Stores and retrieves the most recently chosen resolutions using the [PackageManager] provided by + * the [packageManagerProvider]. + */ +class PackageManagerLastChosenManager( +    private val contentResolver: ContentResolver, +    private val bgDispatcher: CoroutineDispatcher, +    private val targetIntent: Intent, +    private val packageManagerProvider: () -> IPackageManager = AppGlobals::getPackageManager, +) : LastChosenManager { + +    @Throws(RemoteException::class) +    override suspend fun getLastChosen(): ResolveInfo { +        return withContext(bgDispatcher) { +            packageManagerProvider() +                .getLastChosenActivity( +                    targetIntent, +                    targetIntent.resolveTypeIfNeeded(contentResolver), +                    PackageManager.MATCH_DEFAULT_ONLY, +                ) +        } +    } + +    @Throws(RemoteException::class) +    override suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int) { +        return withContext(bgDispatcher) { +            packageManagerProvider() +                .setLastChosenActivity( +                    intent, +                    intent.resolveType(contentResolver), +                    PackageManager.MATCH_DEFAULT_ONLY, +                    filter, +                    match, +                    intent.component, +                ) +        } +    } +} diff --git a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt index 5b5d769c..4ddab755 100644 --- a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt +++ b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt @@ -1,5 +1,5 @@  /* - * Copyright (C) 2022 The Android Open Source Project + * 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. @@ -14,12 +14,8 @@   * limitations under the License.   */ -package com.android.intentresolver.flags +package com.android.intentresolver.v2.listcontroller -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag - -interface FeatureFlagRepository { -    fun isEnabled(flag: UnreleasedFlag): Boolean -    fun isEnabled(flag: ReleasedFlag): Boolean -} +/** Controller for managing lists of [com.android.intentresolver.ResolvedComponentInfo]s. */ +interface ListController : +    LastChosenManager, IntentResolver, ResolvedComponentFiltering, ResolvedComponentSorting diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt new file mode 100644 index 00000000..cae2af95 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt @@ -0,0 +1,34 @@ +package com.android.intentresolver.v2.listcontroller + +import android.app.ActivityManager +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** Class for checking if a permission has been granted. */ +interface PermissionChecker { +    /** Checks if the given [permission] has been granted. */ +    suspend fun checkComponentPermission( +        permission: String, +        uid: Int, +        owningUid: Int, +        exported: Boolean, +    ): Int +} + +/** + * Class for checking if a permission has been granted using the static + * [ActivityManager.checkComponentPermission]. + */ +class ActivityManagerPermissionChecker( +    private val bgDispatcher: CoroutineDispatcher, +) : PermissionChecker { +    override suspend fun checkComponentPermission( +        permission: String, +        uid: Int, +        owningUid: Int, +        exported: Boolean, +    ): Int = +        withContext(bgDispatcher) { +            ActivityManager.checkComponentPermission(permission, uid, owningUid, exported) +        } +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt new file mode 100644 index 00000000..8be45ba2 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt @@ -0,0 +1,39 @@ +/* + * 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.listcontroller + +import android.content.ComponentName +import android.content.SharedPreferences + +/** A class that is able to identify components that should be pinned for the user. */ +interface PinnableComponents { +    /** Whether this component is pinned by the user. */ +    fun isComponentPinned(name: ComponentName): Boolean +} + +/** A class that never pins components. */ +class NoComponentPinning : PinnableComponents { +    override fun isComponentPinned(name: ComponentName): Boolean = false +} + +/** A class that determines pinnable components by user preferences. */ +class SharedPreferencesPinnedComponents( +    private val pinnedSharedPreferences: SharedPreferences, +) : PinnableComponents { +    override fun isComponentPinned(name: ComponentName): Boolean = +        pinnedSharedPreferences.getBoolean(name.flattenToString(), false) +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt new file mode 100644 index 00000000..f0b4bf3f --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt @@ -0,0 +1,69 @@ +package com.android.intentresolver.v2.listcontroller + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ResolveInfo +import android.util.Log +import com.android.intentresolver.ResolvedComponentInfo + +/** A class for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without duplicates. */ +interface ResolveListDeduper { +    /** +     * Adds [ResolveInfo]s in [from] to [ResolvedComponentInfo]s in [into], creating new +     * [ResolvedComponentInfo]s when there is not already a corresponding one. +     * +     * This method may be destructive to both the given [into] list and the underlying +     * [ResolvedComponentInfo]s. +     */ +    fun addToResolveListWithDedupe( +        into: MutableList<ResolvedComponentInfo>, +        intent: Intent, +        from: List<ResolveInfo>, +    ) +} + +/** + * Default implementation for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without + * duplicates. Uses the given [PinnableComponents] to determine the pinning state of newly created + * [ResolvedComponentInfo]s. + */ +class ResolveListDeduperImpl(pinnableComponents: PinnableComponents) : +    ResolveListDeduper, PinnableComponents by pinnableComponents { +    override fun addToResolveListWithDedupe( +        into: MutableList<ResolvedComponentInfo>, +        intent: Intent, +        from: List<ResolveInfo>, +    ) { +        from.forEach { newInfo -> +            if (newInfo.userHandle == null) { +                Log.w(TAG, "Skipping ResolveInfo with no userHandle: $newInfo") +                return@forEach +            } +            val oldInfo = into.firstOrNull { isSameResolvedComponent(newInfo, it) } +            // If existing resolution found, add to existing and filter out +            if (oldInfo != null) { +                oldInfo.add(intent, newInfo) +            } else { +                with(newInfo.activityInfo) { +                    into.add( +                        ResolvedComponentInfo( +                                ComponentName(packageName, name), +                                intent, +                                newInfo, +                            ) +                            .apply { isPinned = isComponentPinned(name) }, +                    ) +                } +            } +        } +    } + +    private fun isSameResolvedComponent(a: ResolveInfo, b: ResolvedComponentInfo): Boolean { +        val ai = a.activityInfo +        return ai.packageName == b.name.packageName && ai.name == b.name.className +    } + +    companion object { +        const val TAG = "ResolveListDeduper" +    } +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt new file mode 100644 index 00000000..e78bff00 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt @@ -0,0 +1,121 @@ +package com.android.intentresolver.v2.listcontroller + +import android.content.pm.PackageManager +import android.util.Log +import com.android.intentresolver.ResolvedComponentInfo +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** Provides filtering methods for lists of [ResolvedComponentInfo]. */ +interface ResolvedComponentFiltering { +    /** +     * Returns a list with all the [ResolvedComponentInfo] in [inputList], less the ones that are +     * not eligible. +     */ +    suspend fun filterIneligibleActivities( +        inputList: List<ResolvedComponentInfo>, +    ): List<ResolvedComponentInfo> + +    /** Filter out any low priority items. */ +    fun filterLowPriority(inputList: List<ResolvedComponentInfo>): List<ResolvedComponentInfo> +} + +/** + * Default instantiation of the filtering methods for lists of [ResolvedComponentInfo]. + * + * Binder calls are performed on the given [bgDispatcher] and permissions are checked as if launched + * from the given [launchedFromUid] UID. Component filtering is handled by the given + * [FilterableComponents] and permission checking is handled by the given [PermissionChecker]. + */ +class ResolvedComponentFilteringImpl( +    private val launchedFromUid: Int, +    filterableComponents: FilterableComponents, +    permissionChecker: PermissionChecker, +) : +    ResolvedComponentFiltering, +    PermissionChecker by permissionChecker, +    FilterableComponents by filterableComponents { +    constructor( +        bgDispatcher: CoroutineDispatcher, +        launchedFromUid: Int, +        filterableComponents: FilterableComponents, +    ) : this( +        launchedFromUid = launchedFromUid, +        filterableComponents = filterableComponents, +        permissionChecker = ActivityManagerPermissionChecker(bgDispatcher), +    ) + +    /** +     * Filter out items that are filtered by [FilterableComponents] or do not have the necessary +     * permissions. +     */ +    override suspend fun filterIneligibleActivities( +        inputList: List<ResolvedComponentInfo>, +    ): List<ResolvedComponentInfo> = coroutineScope { +        inputList +            .map { +                val activityInfo = it.getResolveInfoAt(0).activityInfo +                if (isComponentFiltered(activityInfo.componentName)) { +                    CompletableDeferred(value = null) +                } else { +                    // Do all permission checks in parallel +                    async { +                        val granted = +                            checkComponentPermission( +                                activityInfo.permission, +                                launchedFromUid, +                                activityInfo.applicationInfo.uid, +                                activityInfo.exported, +                            ) == PackageManager.PERMISSION_GRANTED +                        if (granted) it else null +                    } +                } +            } +            .awaitAll() +            .filterNotNull() +    } + +    /** +     * Filters out all elements starting with the first elements with a different priority or +     * default status than the first element. +     */ +    override fun filterLowPriority( +        inputList: List<ResolvedComponentInfo>, +    ): List<ResolvedComponentInfo> { +        val firstResolveInfo = inputList[0].getResolveInfoAt(0) +        // Only display the first matches that are either of equal +        // priority or have asked to be default options. +        val firstDiffIndex = +            inputList.indexOfFirst { resolvedComponentInfo -> +                val resolveInfo = resolvedComponentInfo.getResolveInfoAt(0) +                if (firstResolveInfo == resolveInfo) { +                    false +                } else { +                    if (DEBUG) { +                        Log.v( +                            TAG, +                            "${firstResolveInfo?.activityInfo?.name}=" + +                                "${firstResolveInfo?.priority}/${firstResolveInfo?.isDefault}" + +                                " vs ${resolveInfo?.activityInfo?.name}=" + +                                "${resolveInfo?.priority}/${resolveInfo?.isDefault}" +                        ) +                    } +                    firstResolveInfo!!.priority != resolveInfo!!.priority || +                        firstResolveInfo.isDefault != resolveInfo.isDefault +                } +            } +        return if (firstDiffIndex == -1) { +            inputList +        } else { +            inputList.subList(0, firstDiffIndex) +        } +    } + +    companion object { +        private const val TAG = "ResolvedComponentFilter" +        private const val DEBUG = false +    } +} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt new file mode 100644 index 00000000..8ab41ef0 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt @@ -0,0 +1,108 @@ +package com.android.intentresolver.v2.listcontroller + +import android.os.UserHandle +import android.util.Log +import com.android.intentresolver.ResolvedComponentInfo +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.TargetInfo +import com.android.intentresolver.model.AbstractResolverComparator +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** Provides sorting methods for lists of [ResolvedComponentInfo]. */ +interface ResolvedComponentSorting { +    /** Returns the a copy of the [inputList] sorted by app share score. */ +    suspend fun sorted(inputList: List<ResolvedComponentInfo>?): List<ResolvedComponentInfo>? + +    /** Returns the app share score of the [target]. */ +    fun getScore(target: DisplayResolveInfo): Float + +    /** Returns the app share score of the [targetInfo]. */ +    fun getScore(targetInfo: TargetInfo): Float + +    /** Updates the model about [targetInfo]. */ +    suspend fun updateModel(targetInfo: TargetInfo) + +    /** Updates the model about Activity selection. */ +    suspend fun updateChooserCounts(packageName: String, user: UserHandle, action: String) + +    /** Cleans up resources. Nothing should be called after calling this. */ +    fun destroy() +} + +/** + * Provides sorting methods using the given [resolverComparator]. + * + * Long calculations and binder calls are performed on the given [bgDispatcher]. + */ +class ResolvedComponentSortingImpl( +    private val bgDispatcher: CoroutineDispatcher, +    private val resolverComparator: AbstractResolverComparator, +) : ResolvedComponentSorting { + +    private val computeComplete = AtomicReference<CompletableDeferred<Unit>?>(null) + +    @Throws(InterruptedException::class) +    private suspend fun computeIfNeeded(inputList: List<ResolvedComponentInfo>) { +        if (computeComplete.compareAndSet(null, CompletableDeferred())) { +            resolverComparator.setCallBack { computeComplete.get()!!.complete(Unit) } +            resolverComparator.compute(inputList) +        } +        with(computeComplete.get()!!) { if (isCompleted) return else return await() } +    } + +    override suspend fun sorted( +        inputList: List<ResolvedComponentInfo>?, +    ): List<ResolvedComponentInfo>? { +        if (inputList.isNullOrEmpty()) return inputList + +        return withContext(bgDispatcher) { +            try { +                val beforeRank = System.currentTimeMillis() +                computeIfNeeded(inputList) +                val sorted = inputList.sortedWith(resolverComparator) +                val afterRank = System.currentTimeMillis() +                if (DEBUG) { +                    Log.d(TAG, "Time Cost: ${afterRank - beforeRank}") +                } +                sorted +            } catch (e: InterruptedException) { +                Log.e(TAG, "Compute & Sort was interrupted: $e") +                null +            } +        } +    } + +    override fun getScore(target: DisplayResolveInfo): Float { +        return resolverComparator.getScore(target) +    } + +    override fun getScore(targetInfo: TargetInfo): Float { +        return resolverComparator.getScore(targetInfo) +    } + +    override suspend fun updateModel(targetInfo: TargetInfo) { +        withContext(bgDispatcher) { resolverComparator.updateModel(targetInfo) } +    } + +    override suspend fun updateChooserCounts( +        packageName: String, +        user: UserHandle, +        action: String, +    ) { +        withContext(bgDispatcher) { +            resolverComparator.updateChooserCounts(packageName, user, action) +        } +    } + +    override fun destroy() { +        resolverComparator.destroy() +    } + +    companion object { +        private const val TAG = "ResolvedComponentSort" +        private const val DEBUG = false +    } +} diff --git a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt new file mode 100644 index 00000000..efbf053e --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt @@ -0,0 +1,35 @@ +package com.android.intentresolver.v2.platform + +import android.content.ComponentName +import android.content.res.Resources +import androidx.annotation.StringRes +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Optional +import javax.inject.Qualifier +import javax.inject.Singleton + +internal fun Resources.componentName(@StringRes resId: Int): ComponentName? { +    check(getResourceTypeName(resId) == "string") { "resId must be a string" } +    return ComponentName.unflattenFromString(getString(resId)) +} + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor + +@Module +@InstallIn(SingletonComponent::class) +object ImageEditorModule { +    /** +     * The name of the preferred Activity to launch for editing images. This is added to Intents to +     * edit images using Intent.ACTION_EDIT. +     */ +    @Provides +    @Singleton +    @ImageEditor +    fun imageEditorComponent(@ApplicationOwned resources: Resources) = +        Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor)) +} diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt new file mode 100644 index 00000000..25ee9198 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt @@ -0,0 +1,32 @@ +package com.android.intentresolver.v2.platform + +import android.content.ComponentName +import android.content.res.Resources +import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Optional +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare + +@Module +@InstallIn(SingletonComponent::class) +object NearbyShareModule { + +    @Provides +    @Singleton +    @NearbyShare +    fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) = +        Optional.ofNullable( +            ComponentName.unflattenFromString( +                settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null } +                    ?: resources.getString(R.string.config_defaultNearbySharingComponent), +            ) +        ) +} diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt new file mode 100644 index 00000000..531152ba --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt @@ -0,0 +1,30 @@ +package com.android.intentresolver.v2.platform + +import android.content.ContentResolver +import android.provider.Settings +import javax.inject.Inject + +/** + * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver. + * + * These methods make Binder calls and may block, so use on the Main thread should be avoided. + */ +class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) : +    SecureSettings { + +    override fun getString(name: String): String? { +        return Settings.Secure.getString(resolver, name) +    } + +    override fun getInt(name: String): Int? { +        return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull() +    } + +    override fun getLong(name: String): Long? { +        return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull() +    } + +    override fun getFloat(name: String): Float? { +        return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull() +    } +} diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt new file mode 100644 index 00000000..62ee8ae9 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt @@ -0,0 +1,25 @@ +package com.android.intentresolver.v2.platform + +import android.provider.Settings.SettingNotFoundException + +/** + * A component which provides access to values from [android.provider.Settings.Secure]. + * + * All methods return nullable types instead of throwing [SettingNotFoundException] which yields + * cleaner, more idiomatic Kotlin code: + * + * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO + * + * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value + * missing") + */ +interface SecureSettings { + +    fun getString(name: String): String? + +    fun getInt(name: String): Int? + +    fun getLong(name: String): Long? + +    fun getFloat(name: String): Float? +} diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt new file mode 100644 index 00000000..18f47023 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt @@ -0,0 +1,14 @@ +package com.android.intentresolver.v2.platform + +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface SecureSettingsModule { + +    @Binds @Reusable fun secureSettings(settings: PlatformSecureSettings): SecureSettings +} diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java new file mode 100644 index 00000000..271c6f38 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java @@ -0,0 +1,89 @@ +/* + * 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.ui; + +import android.content.Intent; +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. + */ +public enum ActionTitle { +    VIEW(Intent.ACTION_VIEW, +            R.string.whichViewApplication, +            R.string.whichViewApplicationNamed, +            R.string.whichViewApplicationLabel), +    EDIT(Intent.ACTION_EDIT, +            R.string.whichEditApplication, +            R.string.whichEditApplicationNamed, +            R.string.whichEditApplicationLabel), +    SEND(Intent.ACTION_SEND, +            R.string.whichSendApplication, +            R.string.whichSendApplicationNamed, +            R.string.whichSendApplicationLabel), +    SENDTO(Intent.ACTION_SENDTO, +            R.string.whichSendToApplication, +            R.string.whichSendToApplicationNamed, +            R.string.whichSendToApplicationLabel), +    SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, +            R.string.whichSendApplication, +            R.string.whichSendApplicationNamed, +            R.string.whichSendApplicationLabel), +    CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, +            R.string.whichImageCaptureApplication, +            R.string.whichImageCaptureApplicationNamed, +            R.string.whichImageCaptureApplicationLabel), +    DEFAULT(null, +            R.string.whichApplication, +            R.string.whichApplicationNamed, +            R.string.whichApplicationLabel), +    HOME(Intent.ACTION_MAIN, +            R.string.whichHomeApplication, +            R.string.whichHomeApplicationNamed, +            R.string.whichHomeApplicationLabel); + +    // titles for layout that deals with http(s) intents +    public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; +    public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; +    public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; +    public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; + +    public final String action; +    public final int titleRes; +    public final int namedTitleRes; +    public final @StringRes int labelRes; + +    ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { +        this.action = action; +        this.titleRes = titleRes; +        this.namedTitleRes = namedTitleRes; +        this.labelRes = labelRes; +    } + +    public static ActionTitle forAction(String action) { +        for (ActionTitle title : values()) { +            if (title != HOME && action != null && action.equals(title.action)) { +                return title; +            } +        } +        return DEFAULT; +    } +} diff --git a/java/src/com/android/intentresolver/v2/util/MutableLazy.kt b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt new file mode 100644 index 00000000..4ce9b7fd --- /dev/null +++ b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt @@ -0,0 +1,36 @@ +package com.android.intentresolver.v2.util + +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty + +/** A lazy delegate that can be changed to a new lazy or null at any time. */ +class MutableLazy<T>(initializer: () -> T?) : Lazy<T?> { + +    override val value: T? +        get() = lazy.get()?.value + +    private var lazy: AtomicReference<Lazy<T?>?> = AtomicReference(lazy(initializer)) + +    override fun isInitialized(): Boolean = lazy.get()?.isInitialized() != false + +    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = +        lazy.get()?.getValue(thisRef, property) + +    /** Replace the existing lazy logic with the [newLazy] */ +    fun setLazy(newLazy: Lazy<T?>?) { +        lazy.set(newLazy) +    } + +    /** Replace the existing lazy logic with a [Lazy] created from the [newInitializer]. */ +    fun setLazy(newInitializer: () -> T?) { +        lazy.set(lazy(newInitializer)) +    } + +    /** Set the lazy logic to null. */ +    fun clear() { +        lazy.set(null) +    } +} + +/** Constructs a [MutableLazy] using the given [initializer] */ +fun <T> mutableLazy(initializer: () -> T?) = MutableLazy(initializer) diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt new file mode 100644 index 00000000..9a3cc9c7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -0,0 +1,113 @@ +/* + * 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.validation + +import android.util.Log +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import kotlin.reflect.KClass + +sealed interface Finding { +    val importance: Importance +    val message: String +} + +enum class Importance { +    CRITICAL, +    WARNING, +} + +val Finding.logcatPriority +    get() = +        when (importance) { +            CRITICAL -> Log.ERROR +            else -> Log.WARN +        } + +private fun formatMessage(key: String? = null, msg: String) = buildString { +    key?.also { append("['$key']: ") } +    append(msg) +} + +data class IgnoredValue( +    val key: String, +    val reason: String, +) : Finding { +    override val importance = WARNING + +    override val message: String +        get() = formatMessage(key, "Ignored. $reason") +} + +data class RequiredValueMissing( +    val key: String, +    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" +            ) +} + +data class WrongElementType( +    val key: String, +    override val importance: Importance, +    val container: KClass<*>, +    val actualType: KClass<*>, +    val expectedType: KClass<*> +) : Finding { +    override val message: String +        get() = +            formatMessage( +                key, +                "${container.simpleName} expected with elements of " + +                    "${expectedType.simpleName} " + +                    "but found ${actualType.simpleName} values instead" +            ) +} + +data class ValueIsWrongType( +    val key: String, +    override val importance: Importance, +    val actualType: KClass<*>, +    val allowedTypes: List<KClass<*>>, +) : Finding { + +    override val message: String +        get() = +            formatMessage( +                key, +                "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + +                    "but was ${actualType.simpleName}" +            ) +} + +data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { +    override val importance: Importance +        get() = CRITICAL +    override val message: String +        get() = +            formatMessage( +                key, +                "An unhandled exception was caught during validation: " + +                    thrown.stackTraceToString() +            ) +} diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt new file mode 100644 index 00000000..46939602 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -0,0 +1,129 @@ +/* + * 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.validation + +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING + +/** + * Provides a mechanism for validating a result from a set of properties. + * + * The results of validation are provided as [findings]. + */ +interface Validation { +    val findings: List<Finding> + +    /** +     * Require a valid property. +     * +     * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. +     * +     * @param property the required property +     * @return a valid **T** +     */ +    @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T + +    /** +     * Request an optional value for a property. +     * +     * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. +     * +     * @param property the required property +     * @return a valid **T** +     */ +    fun <T> optional(property: Validator<T>): T? + +    /** +     * Report a property as __ignored__. +     * +     * The presence of any value will report a warning citing [reason]. +     */ +    fun <T> ignored(property: Validator<T>, reason: String) +} + +/** Performs validation for a specific key -> value pair. */ +interface Validator<T> { +    val key: String + +    /** +     * Performs validation on a specific value from [source]. +     * +     * @param source a source for reading the property value. Values are intentionally untyped +     *   (Any?) to avoid upstream code from making type assertions through type inference. Types are +     *   asserted later using a [Validator]. +     * @param importance the importance of any findings +     */ +    fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> +} + +internal class InvalidResultError internal constructor() : Error() + +/** + * Perform a number of validations on the source, assembling and returning a Result. + * + * When an exception is thrown by [validate], it is caught here. In response, a failed + * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. + * + * @param validate perform validations and return a [ValidationResult] + */ +fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> { +    val validation = ValidationImpl(source) +    return runCatching { validate(validation) } +        .fold( +            onSuccess = { result -> Valid(result, validation.findings) }, +            onFailure = { +                when (it) { +                    // A validator has interrupted validation. Return the findings. +                    is InvalidResultError -> Invalid(validation.findings) + +                    // Some other exception was thrown from [validate], +                    else -> Invalid(findings = listOf(UncaughtException(it))) +                } +            } +        ) +} + +private class ValidationImpl(val source: (String) -> Any?) : Validation { +    override val findings = mutableListOf<Finding>() + +    override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING) + +    override fun <T> required(property: Validator<T>): T { +        return validate(property, CRITICAL) ?: throw InvalidResultError() +    } + +    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. +            findings += IgnoredValue(property.key, reason) +        } +    } + +    private fun <T> validate(property: Validator<T>, importance: Importance): T? { +        return runCatching { property.validate(source, importance) } +            .fold( +                onSuccess = { result -> +                    findings += result.findings +                    result.value +                }, +                onFailure = { +                    findings += UncaughtException(it, property.key) +                    null +                } +            ) +    } +} diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt new file mode 100644 index 00000000..092cabe8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -0,0 +1,39 @@ +/* + * 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.validation + +import android.util.Log + +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>(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 +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt new file mode 100644 index 00000000..3cefeb15 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -0,0 +1,59 @@ +/* + * 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.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.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType + +class IntentOrUri(override val key: String) : Validator<Intent> { + +    override fun validate( +        source: (String) -> Any?, +        importance: Importance +    ): ValidationResult<Intent> { + +        return when (val value = source(key)) { +            // An intent, return it. +            is Intent -> Valid(value) + +            // A Uri was supplied. +            // Unfortunately, converting Uri -> Intent requires a toString(). +            is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) + +            // No value present. +            null -> createResult(importance, RequiredValueMissing(key, Intent::class)) + +            // Some other type. +            else -> { +                return createResult( +                    importance, +                    ValueIsWrongType( +                        key, +                        importance, +                        actualType = value::class, +                        allowedTypes = listOf(Intent::class, Uri::class) +                    ) +                ) +            } +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt new file mode 100644 index 00000000..c6c4abba --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -0,0 +1,83 @@ +/* + * 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.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.android.intentresolver.v2.validation.WrongElementType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class ParceledArray<T : Any>( +    override val key: String, +    private val elementType: KClass<T>, +) : Validator<List<T>> { + +    override fun validate( +        source: (String) -> Any?, +        importance: Importance +    ): ValidationResult<List<T>> { + +        return when (val value: Any? = source(key)) { +            // No value present. +            null -> createResult(importance, RequiredValueMissing(key, 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>. + +            // To handle this safely, treat as Array<*>, assert contents of the expected +            // parcelable type, and return as a list. + +            is Array<*> -> { +                val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } +                when (invalid) { +                    // No invalid elements, result is ok. +                    null -> Valid(value.map { elementType.cast(it) }) + +                    // At least one incorrect element type found. +                    else -> +                        createResult( +                            importance, +                            WrongElementType( +                                key, +                                importance, +                                actualType = invalid::class, +                                container = Array::class, +                                expectedType = elementType +                            ) +                        ) +                } +            } + +            // The value is not an Array at all. +            else -> +                createResult( +                    importance, +                    ValueIsWrongType( +                        key, +                        importance, +                        actualType = value::class, +                        allowedTypes = listOf(elementType) +                    ) +                ) +        } +    } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt new file mode 100644 index 00000000..3287b84b --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -0,0 +1,54 @@ +/* + * 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.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class SimpleValue<T : Any>( +    override val key: String, +    private val expected: KClass<T>, +) : Validator<T> { + +    override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> { +        val value: Any? = source(key) +        return when { +            // The value is present and of the expected type. +            expected.isInstance(value) -> return Valid(expected.cast(value)) + +            // No value is present. +            value == null -> createResult(importance, RequiredValueMissing(key, expected)) + +            // The value is some other type. +            else -> +                createResult( +                    importance, +                    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 new file mode 100644 index 00000000..4e6e5dff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.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.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> { +    return SimpleValue(key, T::class) +} + +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/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt new file mode 100644 index 00000000..26464ca1 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt @@ -0,0 +1,90 @@ +package com.android.intentresolver.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.ScrollingView +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop +import androidx.core.widget.NestedScrollView + +/** + * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to + * orchestrate content preview scrolling. It expects one [LinearLayout] child with + * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child + * will be made scrollable (it is expected to be a content preview view). + */ +class ChooserNestedScrollView : NestedScrollView { +    constructor(context: Context) : super(context) +    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) +    constructor( +        context: Context, +        attrs: AttributeSet?, +        defStyleAttr: Int +    ) : super(context, attrs, defStyleAttr) + +    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { +        val content = +            getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected") +        require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" } +        require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { +            "Expected to have an exact width" +        } + +        val lp = content.layoutParams ?: error("LayoutParams is missing") +        val contentWidthSpec = +            getChildMeasureSpec( +                widthMeasureSpec, +                paddingLeft + content.marginLeft + content.marginRight + paddingRight, +                lp.width +            ) +        val contentHeightSpec = +            getChildMeasureSpec( +                heightMeasureSpec, +                paddingTop + content.marginTop + content.marginBottom + paddingBottom, +                lp.height +            ) +        content.measure(contentWidthSpec, contentHeightSpec) + +        if (content.childCount > 1) { +            // We expect that the first child should be scrollable up +            val child = content.getChildAt(0) +            val height = +                MeasureSpec.getSize(heightMeasureSpec) + +                    child.measuredHeight + +                    child.marginTop + +                    child.marginBottom + +            content.measure( +                contentWidthSpec, +                MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) +            ) +        } +        setMeasuredDimension( +            MeasureSpec.getSize(widthMeasureSpec), +            minOf( +                MeasureSpec.getSize(heightMeasureSpec), +                paddingTop + +                    content.marginTop + +                    content.measuredHeight + +                    content.marginBottom + +                    paddingBottom +            ) +        ) +    } + +    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { +        // let the parent scroll +        super.onNestedPreScroll(target, dx, dy, consumed, type) +        // scroll ourselves, if recycler has not scrolled +        val delta = dy - consumed[1] +        if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) { +            val preScrollY = scrollY +            scrollBy(0, delta) +            consumed[1] += scrollY - preScrollY +        } +    } +} diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java index de76a1d2..2c8140d9 100644 --- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java +++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java @@ -19,7 +19,6 @@ package com.android.intentresolver.widget;  import static android.content.res.Resources.ID_NULL; -import android.annotation.IdRes;  import android.content.Context;  import android.content.res.TypedArray;  import android.graphics.Canvas; @@ -45,6 +44,10 @@ import android.view.animation.AnimationUtils;  import android.widget.AbsListView;  import android.widget.OverScroller; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ScrollingView;  import androidx.recyclerview.widget.RecyclerView;  import com.android.intentresolver.R; @@ -131,6 +134,9 @@ public class ResolverDrawerLayout extends ViewGroup {      private AbsListView mNestedListChild;      private RecyclerView mNestedRecyclerChild; +    @Nullable +    private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate; +      private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =              new ViewTreeObserver.OnTouchModeChangeListener() {                  @Override @@ -167,6 +173,12 @@ public class ResolverDrawerLayout extends ViewGroup {              mIgnoreOffsetTopLimitViewId = a.getResourceId(                      R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);          } +        mFlingLogicDelegate = +                a.getBoolean( +                        R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic, +                        false) +                    ? new ScrollablePreviewFlingLogicDelegate() {} +                    : null;          a.recycle();          mScrollIndicatorDrawable = mContext.getDrawable( @@ -832,6 +844,9 @@ public class ResolverDrawerLayout extends ViewGroup {      @Override      public boolean onNestedPreFling(View target, float velocityX, float velocityY) { +        if (mFlingLogicDelegate != null) { +            return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY); +        }          if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {              smoothScrollTo(0, velocityY);              return true; @@ -841,9 +856,12 @@ public class ResolverDrawerLayout extends ViewGroup {      @Override      public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { +        if (mFlingLogicDelegate != null) { +            return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed); +        }          // TODO: find a more suitable way to fix it.          //  RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, -        //  previously the value was based whether the fling can be performed in given direction +        //  previously the value was based on whether the fling can be performed in given direction          //  i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a          //  workaround that restores the legacy functionality.          boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity) @@ -885,6 +903,13 @@ public class ResolverDrawerLayout extends ViewGroup {                  && firstChild.getTop() >= recyclerView.getPaddingTop();      } +    private static boolean isFlingTargetAtTop(View target) { +        if (target instanceof ScrollingView) { +            return !target.canScrollVertically(-1); +        } +        return false; +    } +      private boolean performAccessibilityActionCommon(int action) {          switch (action) {              case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: @@ -974,7 +999,7 @@ public class ResolverDrawerLayout extends ViewGroup {      }      @Override -    public void onDrawForeground(Canvas canvas) { +    public void onDrawForeground(@NonNull Canvas canvas) {          if (mScrollIndicatorDrawable != null) {              mScrollIndicatorDrawable.draw(canvas);          } @@ -1299,4 +1324,74 @@ public class ResolverDrawerLayout extends ViewGroup {          }          return mMetricsLogger;      } + +    /** +     * Controlled by +     * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW} +     */ +    private interface ScrollablePreviewFlingLogicDelegate { +        default boolean onNestedPreFling( +                ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) { +            boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity +                    && drawer.mCollapseOffset != 0; +            if (shouldScroll) { +                drawer.smoothScrollTo(0, velocityY); +                return true; +            } +            boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity) +                    && velocityY < 0 +                    && isFlingTargetAtTop(target); +            if (shouldDismiss) { +                if (drawer.getShowAtTop()) { +                    drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); +                } else { +                    if (drawer.isDismissable() +                            && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { +                        drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); +                        drawer.mDismissOnScrollerFinished = true; +                    } else { +                        drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); +                    } +                } +                return true; +            } +            return false; +        } + +        default boolean onNestedFling( +                ResolverDrawerLayout drawer, +                View target, +                float velocityX, +                float velocityY, +                boolean consumed) { +            // TODO: find a more suitable way to fix it. +            //  RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, +            //  previously the value was based on whether the fling can be performed in given +            //  direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop +            //  method is a workaround that restores the legacy functionality. +            boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed; +            if (shouldConsume) { +                if (drawer.getShowAtTop()) { +                    if (drawer.isDismissable() && velocityY > 0) { +                        drawer.abortAnimation(); +                        drawer.dismiss(); +                    } else { +                        drawer.smoothScrollTo( +                                velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY); +                    } +                } else { +                    if (drawer.isDismissable() +                            && velocityY < 0 +                            && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { +                        drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); +                        drawer.mDismissOnScrollerFinished = true; +                    } else { +                        drawer.smoothScrollTo( +                                velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY); +                    } +                } +            } +            return shouldConsume; +        } +    }  } diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 3bbafc40..7fe16091 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -26,11 +26,16 @@ import android.util.TypedValue  import android.view.LayoutInflater  import android.view.View  import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener +import android.view.animation.DecelerateInterpolator  import android.widget.ImageView  import android.widget.TextView  import androidx.annotation.VisibleForTesting  import androidx.constraintlayout.widget.ConstraintLayout  import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.DefaultItemAnimator  import androidx.recyclerview.widget.LinearLayoutManager  import androidx.recyclerview.widget.RecyclerView  import com.android.intentresolver.R @@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow  import kotlinx.coroutines.flow.takeWhile  import kotlinx.coroutines.joinAll  import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine  private const val TRANSITION_NAME = "screenshot_preview_image"  private const val PLURALS_COUNT = "count" @@ -65,7 +71,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {          defStyleAttr: Int      ) : super(context, attrs, defStyleAttr) {          layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) -        adapter = Adapter(context)          context              .obtainStyledAttributes(attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0) @@ -98,11 +103,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {                              )                              .toInt()                  } -                addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing)) +                super.addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))                  maxWidthHint =                      a.getDimensionPixelSize(R.styleable.ScrollableImagePreviewView_maxWidthHint, -1)              } +        val itemAnimator = ItemAnimator() +        super.setItemAnimator(itemAnimator) +        super.setAdapter(Adapter(context, itemAnimator.getAddDuration()))      }      private var batchLoader: BatchPreviewLoader? = null @@ -167,6 +175,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {          return null      } +    override fun setAdapter(adapter: RecyclerView.Adapter<*>?) { +        error("This method is not supported") +    } + +    override fun setItemAnimator(animator: RecyclerView.ItemAnimator?) { +        error("This method is not supported") +    } +      fun setImageLoader(imageLoader: CachingImageLoader) {          previewAdapter.imageLoader = imageLoader      } @@ -269,7 +285,10 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {          File      } -    private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { +    private class Adapter( +        private val context: Context, +        private val fadeInDurationMs: Long, +    ) : RecyclerView.Adapter<ViewHolder>() {          private val previews = ArrayList<Preview>()          private val imagePreviewDescription =              context.resources.getString(R.string.image_preview_a11y_description) @@ -311,15 +330,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {              if (newPreviews.isEmpty()) return              val insertPos = previews.size              val hadOtherItem = hasOtherItem -            val wasEmpty = previews.isEmpty() +            val oldItemCount = getItemCount()              previews.addAll(newPreviews)              if (firstImagePos < 0) {                  val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }                  if (pos >= 0) firstImagePos = insertPos + pos              } -            if (wasEmpty) { -                // we don't want any item animation in that case -                notifyDataSetChanged() +            if (insertPos == 0) { +                if (oldItemCount > 0) { +                    notifyItemRangeRemoved(0, oldItemCount) +                } +                notifyItemRangeInserted(insertPos, getItemCount())              } else {                  notifyItemRangeInserted(insertPos, newPreviews.size)                  when { @@ -366,6 +387,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {                      vh.bind(                          previews[position],                          imageLoader ?: error("ImageLoader is missing"), +                        fadeInDurationMs,                          isSharedTransitionElement = position == firstImagePos,                          previewReadyCallback =                              if ( @@ -416,10 +438,13 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {          fun bind(              preview: Preview,              imageLoader: CachingImageLoader, +            fadeInDurationMs: Long,              isSharedTransitionElement: Boolean,              previewReadyCallback: ((String) -> Unit)?          ) {              image.setImageDrawable(null) +            image.alpha = 1f +            image.clearAnimation()              (image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params ->                  params.dimensionRatio = preview.aspectRatioString              } @@ -453,11 +478,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {              }              resetScope().launch {                  loadImage(preview, imageLoader) -                if (preview.type == PreviewType.Image) { -                    previewReadyCallback?.let { callback -> -                        image.waitForPreDraw() -                        callback(TRANSITION_NAME) -                    } +                if (preview.type == PreviewType.Image && previewReadyCallback != null) { +                    image.waitForPreDraw() +                    previewReadyCallback(TRANSITION_NAME) +                } else if (image.isAttachedToWindow()) { +                    fadeInPreview(fadeInDurationMs)                  }              }          } @@ -473,6 +498,30 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {              image.setImageBitmap(bitmap)          } +        private suspend fun fadeInPreview(durationMs: Long) = +            suspendCancellableCoroutine { continuation -> +                val animation = +                    AlphaAnimation(0f, 1f).apply { +                        duration = durationMs +                        interpolator = DecelerateInterpolator() +                        setAnimationListener( +                            object : AnimationListener { +                                override fun onAnimationStart(animation: Animation?) = Unit +                                override fun onAnimationRepeat(animation: Animation?) = Unit + +                                override fun onAnimationEnd(animation: Animation?) { +                                    continuation.resumeWith(Result.success(Unit)) +                                } +                            } +                        ) +                    } +                image.startAnimation(animation) +                continuation.invokeOnCancellation { +                    image.clearAnimation() +                    image.alpha = 1f +                } +            } +          private fun resetScope(): CoroutineScope =              CoroutineScope(Dispatchers.Main.immediate).also {                  scope?.cancel() @@ -521,6 +570,70 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {          }      } +    /** +     * ItemAnimator to handle a special case of addng first image items into the view. The view is +     * used with wrap_content width spec thus after adding the first views it, generally, changes +     * its size and position breaking the animation. This class handles that by preserving loading +     * idicator position in this special case. +     */ +    private inner class ItemAnimator() : DefaultItemAnimator() { +        private var animatedVH: ViewHolder? = null +        private var originalTranslation = 0f + +        override fun recordPreLayoutInformation( +            state: State, +            viewHolder: RecyclerView.ViewHolder, +            changeFlags: Int, +            payloads: MutableList<Any> +        ): ItemHolderInfo { +            return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads).let { +                holderInfo -> +                if (viewHolder is LoadingItemViewHolder && getChildCount() == 1) { +                    LoadingItemHolderInfo(holderInfo, parentLeft = left) +                } else { +                    holderInfo +                } +            } +        } + +        override fun animateDisappearance( +            viewHolder: RecyclerView.ViewHolder, +            preLayoutInfo: ItemHolderInfo, +            postLayoutInfo: ItemHolderInfo? +        ): Boolean { +            if (viewHolder is LoadingItemViewHolder && preLayoutInfo is LoadingItemHolderInfo) { +                val view = viewHolder.itemView +                animatedVH = viewHolder +                originalTranslation = view.getTranslationX() +                view.setTranslationX( +                    (preLayoutInfo.parentLeft - left + preLayoutInfo.left).toFloat() - view.left +                ) +            } +            return super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo) +        } + +        override fun onRemoveFinished(viewHolder: RecyclerView.ViewHolder) { +            if (animatedVH === viewHolder) { +                viewHolder.itemView.setTranslationX(originalTranslation) +                animatedVH = null +            } +            super.onRemoveFinished(viewHolder) +        } + +        private inner class LoadingItemHolderInfo( +            holderInfo: ItemHolderInfo, +            val parentLeft: Int, +        ) : ItemHolderInfo() { +            init { +                left = holderInfo.left +                top = holderInfo.top +                right = holderInfo.right +                bottom = holderInfo.bottom +                changeFlags = holderInfo.changeFlags +            } +        } +    } +      @VisibleForTesting      class BatchPreviewLoader(          private val imageLoader: CachingImageLoader, diff --git a/java/tests/Android.bp b/java/tests/Android.bp deleted file mode 100644 index e10ca72a..00000000 --- a/java/tests/Android.bp +++ /dev/null @@ -1,47 +0,0 @@ -package { -    // See: http://go/android-license-faq -    default_applicable_licenses: ["packages_modules_IntentResolver_license"], -} - -android_test { -    name: "IntentResolverUnitTests", - -    // Include all test java files. -    srcs: [ -        "src/**/*.java", -        "src/**/*.kt", -    ], - -    libs: [ -        "android.test.runner", -        "android.test.base", -        "android.test.mock", -        "framework", -        "framework-res", -    ], - -    static_libs: [ -        "IntentResolver-core", -        "androidx.test.core", -        "androidx.test.rules", -        "androidx.test.ext.junit", -        "androidx.test.ext.truth", -        "androidx.test.espresso.contrib", -        "androidx.test.espresso.core", -        "androidx.test.rules", -        "androidx.lifecycle_lifecycle-common-java8", -        "androidx.lifecycle_lifecycle-extensions", -        "androidx.lifecycle_lifecycle-runtime-ktx", -        "androidx.lifecycle_lifecycle-runtime-testing", -        "kotlinx_coroutines_test", -        "mockito-target-minus-junit4", -        "testables", -        "truth", -    ], -    plugins: ["dagger2-compiler"], -    test_suites: ["general-tests"], -    sdk_version: "core_platform", -    compile_multilib: "both", - -    dont_merge_manifests: true, -} diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml deleted file mode 100644 index 05830c4c..00000000 --- a/java/tests/AndroidManifest.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2021 The Android Open Source Project - -     Licensed under the Apache License, Version 2.0 (the "License"); -     you may not use this file except in compliance with the License. -     You may obtain a copy of the License at - -          http://www.apache.org/licenses/LICENSE-2.0 - -     Unless required by applicable law or agreed to in writing, software -     distributed under the License is distributed on an "AS IS" BASIS, -     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -     See the License for the specific language governing permissions and -     limitations under the License. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" -          package="com.android.intentresolver.tests"> - -    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" /> - -    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> -    <uses-permission android:name="android.permission.QUERY_USERS"/> -    <uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"/> -    <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/> -    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> - -    <application android:name="com.android.intentresolver.TestApplication"> -        <uses-library android:name="android.test.runner" /> -        <activity android:name="com.android.intentresolver.ChooserWrapperActivity" /> -        <activity android:name="com.android.intentresolver.ResolverWrapperActivity" /> -        <provider -            android:authorities="com.android.intentresolver.tests" -            android:name="com.android.intentresolver.TestContentProvider" -            android:grantUriPermissions="true" /> -    </application> - -    <instrumentation android:name="android.testing.TestableInstrumentation" -        android:targetPackage="com.android.intentresolver.tests" -        android:label="Tests for IntentResolver"> -    </instrumentation> - -</manifest> diff --git a/java/tests/AndroidTest.xml b/java/tests/AndroidTest.xml deleted file mode 100644 index d1d77c10..00000000 --- a/java/tests/AndroidTest.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2021 The Android Open Source Project - -     Licensed under the Apache License, Version 2.0 (the "License"); -     you may not use this file except in compliance with the License. -     You may obtain a copy of the License at - -          http://www.apache.org/licenses/LICENSE-2.0 - -     Unless required by applicable law or agreed to in writing, software -     distributed under the License is distributed on an "AS IS" BASIS, -     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -     See the License for the specific language governing permissions and -     limitations under the License. ---> -<configuration description="Run IntentResolver Tests."> -    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> -        <option name="test-file-name" value="IntentResolverUnitTests.apk" /> -    </target_preparer> - -    <option name="test-suite-tag" value="apct" /> -    <option name="test-tag" value="IntentResolverUnitTests" /> -    <test class="com.android.tradefed.testtype.AndroidJUnitTest" > -        <option name="package" value="com.android.intentresolver.tests" /> -        <option name="runner" value="android.testing.TestableInstrumentation" /> -        <option name="hidden-api-checks" value="false"/> -    </test> -</configuration> diff --git a/java/tests/res/drawable/test320x240.png b/java/tests/res/drawable/test320x240.png Binary files differdeleted file mode 100644 index 9b5800da..00000000 --- a/java/tests/res/drawable/test320x240.png +++ /dev/null diff --git a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt b/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt deleted file mode 100644 index cd2fbc7a..00000000 --- a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt +++ /dev/null @@ -1,79 +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 - -import android.os.UserHandle - -import com.google.common.truth.Truth.assertThat - -import org.junit.Test - -class AnnotatedUserHandlesTest { - -    @Test -    fun testBasicProperties() {  // Fields that are reflected back w/o logic. -        val info = AnnotatedUserHandles.newBuilder() -            .setUserIdOfCallingApp(42) -            .setUserHandleSharesheetLaunchedAs(UserHandle.of(116)) -            .setPersonalProfileUserHandle(UserHandle.of(117)) -            .setWorkProfileUserHandle(UserHandle.of(118)) -            .setCloneProfileUserHandle(UserHandle.of(119)) -            .build() - -        assertThat(info.userIdOfCallingApp).isEqualTo(42) -        assertThat(info.userHandleSharesheetLaunchedAs.identifier).isEqualTo(116) -        assertThat(info.personalProfileUserHandle.identifier).isEqualTo(117) -        assertThat(info.workProfileUserHandle?.identifier).isEqualTo(118) -        assertThat(info.cloneProfileUserHandle?.identifier).isEqualTo(119) -    } - -    @Test -    fun testWorkTabInitiallySelectedWhenLaunchedFromWorkProfile() { -        val info = AnnotatedUserHandles.newBuilder() -            .setUserIdOfCallingApp(42) -            .setPersonalProfileUserHandle(UserHandle.of(101)) -            .setWorkProfileUserHandle(UserHandle.of(202)) -            .setUserHandleSharesheetLaunchedAs(UserHandle.of(202)) -            .build() - -        assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(202) -    } - -    @Test -    fun testPersonalTabInitiallySelectedWhenLaunchedFromPersonalProfile() { -        val info = AnnotatedUserHandles.newBuilder() -            .setUserIdOfCallingApp(42) -            .setPersonalProfileUserHandle(UserHandle.of(101)) -            .setWorkProfileUserHandle(UserHandle.of(202)) -            .setUserHandleSharesheetLaunchedAs(UserHandle.of(101)) -            .build() - -        assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101) -    } - -    @Test -    fun testPersonalTabInitiallySelectedWhenLaunchedFromOtherProfile() { -        val info = AnnotatedUserHandles.newBuilder() -            .setUserIdOfCallingApp(42) -            .setPersonalProfileUserHandle(UserHandle.of(101)) -            .setWorkProfileUserHandle(UserHandle.of(202)) -            .setUserHandleSharesheetLaunchedAs(UserHandle.of(303)) -            .build() - -        assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101) -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt deleted file mode 100644 index af6e5f16..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ /dev/null @@ -1,225 +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 - -import android.app.Activity -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.res.Resources -import android.graphics.drawable.Icon -import android.service.chooser.ChooserAction -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.logging.EventLog -import com.google.common.collect.ImmutableList -import com.google.common.truth.Truth.assertThat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.function.Consumer -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito - -@RunWith(AndroidJUnit4::class) -class ChooserActionFactoryTest { -    private val context = InstrumentationRegistry.getInstrumentation().getContext() - -    private val logger = mock<EventLog>() -    private val actionLabel = "Action label" -    private val modifyShareLabel = "Modify share" -    private val testAction = "com.android.intentresolver.testaction" -    private val countdown = CountDownLatch(1) -    private val testReceiver: BroadcastReceiver = -        object : BroadcastReceiver() { -            override fun onReceive(context: Context, intent: Intent) { -                // Just doing at most a single countdown per test. -                countdown.countDown() -            } -        } -    private val resultConsumer = -        object : Consumer<Int> { -            var latestReturn = Integer.MIN_VALUE - -            override fun accept(resultCode: Int) { -                latestReturn = resultCode -            } -        } - -    @Before -    fun setup() { -        context.registerReceiver(testReceiver, IntentFilter(testAction)) -    } - -    @After -    fun teardown() { -        context.unregisterReceiver(testReceiver) -    } - -    @Test -    fun testCreateCustomActions() { -        val factory = createFactory() - -        val customActions = factory.createCustomActions() - -        assertThat(customActions.size).isEqualTo(1) -        assertThat(customActions[0].label).isEqualTo(actionLabel) - -        // click it -        customActions[0].onClicked.run() - -        Mockito.verify(logger).logCustomActionSelected(eq(0)) -        assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) -        // Verify the pending intent has been called -        countdown.await(500, TimeUnit.MILLISECONDS) -    } - -    @Test -    fun testNoModifyShareAction() { -        val factory = createFactory(includeModifyShare = false) - -        assertThat(factory.modifyShareAction).isNull() -    } - -    @Test -    fun testModifyShareAction() { -        val factory = createFactory(includeModifyShare = true) - -        val action = factory.modifyShareAction ?: error("Modify share action should not be null") -        action.onClicked.run() - -        Mockito.verify(logger) -            .logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE)) -        assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) -        // Verify the pending intent has been called -        countdown.await(500, TimeUnit.MILLISECONDS) -    } - -    @Test -    fun nonSendAction_noCopyRunnable() { -        val targetIntent = -            Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                putExtra(Intent.EXTRA_TEXT, "Text to show") -            } - -        val chooserRequest = -            mock<ChooserRequestParameters> { -                whenever(this.targetIntent).thenReturn(targetIntent) -                whenever(chooserActions).thenReturn(ImmutableList.of()) -            } -        val testSubject = -            ChooserActionFactory( -                context, -                chooserRequest, -                mock(), -                logger, -                {}, -                { null }, -                mock(), -                {}, -            ) -        assertThat(testSubject.copyButtonRunnable).isNull() -    } - -    @Test -    fun sendActionNoText_noCopyRunnable() { -        val targetIntent = Intent(Intent.ACTION_SEND) - -        val chooserRequest = -            mock<ChooserRequestParameters> { -                whenever(this.targetIntent).thenReturn(targetIntent) -                whenever(chooserActions).thenReturn(ImmutableList.of()) -            } -        val testSubject = -            ChooserActionFactory( -                context, -                chooserRequest, -                mock(), -                logger, -                {}, -                { null }, -                mock(), -                {}, -            ) -        assertThat(testSubject.copyButtonRunnable).isNull() -    } - -    @Test -    fun sendActionWithText_nonNullCopyRunnable() { -        val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Text") } - -        val chooserRequest = -            mock<ChooserRequestParameters> { -                whenever(this.targetIntent).thenReturn(targetIntent) -                whenever(chooserActions).thenReturn(ImmutableList.of()) -            } -        val testSubject = -            ChooserActionFactory( -                context, -                chooserRequest, -                mock(), -                logger, -                {}, -                { null }, -                mock(), -                {}, -            ) -        assertThat(testSubject.copyButtonRunnable).isNotNull() -    } - -    private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory { -        val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction), 0) -        val targetIntent = Intent() -        val action = -            ChooserAction.Builder( -                    Icon.createWithResource("", Resources.ID_NULL), -                    actionLabel, -                    testPendingIntent -                ) -                .build() -        val chooserRequest = mock<ChooserRequestParameters>() -        whenever(chooserRequest.targetIntent).thenReturn(targetIntent) -        whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) - -        if (includeModifyShare) { -            val modifyShare = -                ChooserAction.Builder( -                        Icon.createWithResource("", Resources.ID_NULL), -                        modifyShareLabel, -                        testPendingIntent -                    ) -                    .build() -            whenever(chooserRequest.modifyShareAction).thenReturn(modifyShare) -        } - -        return ChooserActionFactory( -            context, -            chooserRequest, -            mock(), -            logger, -            {}, -            { null }, -            mock(), -            resultConsumer -        ) -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java deleted file mode 100644 index 84f5124c..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.database.Cursor; -import android.os.UserHandle; - -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.contentpreview.ImageLoader; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.logging.EventLog; -import com.android.intentresolver.shortcuts.ShortcutLoader; - -import java.util.function.Consumer; -import java.util.function.Function; - -import kotlin.jvm.functions.Function2; - -/** - * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing. - * We cannot directly mock the activity created since instrumentation creates it, so instead we use - * this singleton to modify behavior. - */ -public class ChooserActivityOverrideData { -    private static ChooserActivityOverrideData sInstance = null; - -    public static ChooserActivityOverrideData getInstance() { -        if (sInstance == null) { -            sInstance = new ChooserActivityOverrideData(); -        } -        return sInstance; -    } - -    @SuppressWarnings("Since15") -    public Function<PackageManager, PackageManager> createPackageManager; -    public Function<TargetInfo, Boolean> onSafelyStartInternalCallback; -    public Function<TargetInfo, Boolean> onSafelyStartCallback; -    public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> -            shortcutLoaderFactory = (userHandle, callback) -> null; -    public ChooserActivity.ChooserListController resolverListController; -    public ChooserActivity.ChooserListController workResolverListController; -    public Boolean isVoiceInteraction; -    public Cursor resolverCursor; -    public boolean resolverForceException; -    public ImageLoader imageLoader; -    public EventLog mEventLog; -    public int alternateProfileSetting; -    public Resources resources; -    public UserHandle workProfileUserHandle; -    public UserHandle cloneProfileUserHandle; -    public UserHandle tabOwnerUserHandleForLaunch; -    public boolean hasCrossProfileIntents; -    public boolean isQuietModeEnabled; -    public Integer myUserId; -    public WorkProfileAvailabilityManager mWorkProfileAvailability; -    public CrossProfileIntentsChecker mCrossProfileIntentsChecker; -    public PackageManager packageManager; -    public FeatureFlagRepository featureFlagRepository; - -    public void reset() { -        onSafelyStartInternalCallback = null; -        isVoiceInteraction = null; -        createPackageManager = null; -        imageLoader = null; -        resolverCursor = null; -        resolverForceException = false; -        resolverListController = mock(ChooserActivity.ChooserListController.class); -        workResolverListController = mock(ChooserActivity.ChooserListController.class); -        mEventLog = mock(EventLog.class); -        alternateProfileSetting = 0; -        resources = null; -        workProfileUserHandle = null; -        cloneProfileUserHandle = null; -        tabOwnerUserHandleForLaunch = null; -        hasCrossProfileIntents = true; -        isQuietModeEnabled = false; -        myUserId = null; -        packageManager = null; -        mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { -            @Override -            public boolean isQuietModeEnabled() { -                return isQuietModeEnabled; -            } - -            @Override -            public boolean isWorkProfileUserUnlocked() { -                return true; -            } - -            @Override -            public void requestQuietModeEnabled(boolean enabled) { -                isQuietModeEnabled = enabled; -            } - -            @Override -            public void markWorkProfileEnabledBroadcastReceived() {} - -            @Override -            public boolean isWaitingToEnableWorkProfile() { -                return false; -            } -        }; -        shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); - -        mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); -        when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) -                .thenAnswer(invocation -> hasCrossProfileIntents); -        featureFlagRepository = null; -    } - -    private ChooserActivityOverrideData() {} -} - diff --git a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt b/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt deleted file mode 100644 index 9a5dabdb..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt +++ /dev/null @@ -1,71 +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 - -import android.content.ComponentName -import android.provider.Settings -import android.testing.TestableContext -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ChooserIntegratedDeviceComponentsTest { -    private val secureSettings = mock<SecureSettings>() -    private val testableContext = -        TestableContext(InstrumentationRegistry.getInstrumentation().getContext()) - -    @Test -    fun testEditorAndNearby() { -        val resources = testableContext.getOrCreateTestableResources() - -        resources.addOverride(R.string.config_systemImageEditor, "") -        resources.addOverride(R.string.config_defaultNearbySharingComponent, "") - -        var components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) - -        assertThat(components.editSharingComponent).isNull() -        assertThat(components.nearbySharingComponent).isNull() - -        val editor = ComponentName.unflattenFromString("com.android/com.android.Editor") -        val nearby = ComponentName.unflattenFromString("com.android/com.android.nearby") - -        resources.addOverride(R.string.config_systemImageEditor, editor?.flattenToString()) -        resources.addOverride( -            R.string.config_defaultNearbySharingComponent, nearby?.flattenToString()) - -        components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) - -        assertThat(components.editSharingComponent).isEqualTo(editor) -        assertThat(components.nearbySharingComponent).isEqualTo(nearby) - -        val anotherNearby = -            ComponentName.unflattenFromString("com.android/com.android.another_nearby") -        whenever( -            secureSettings.getString( -                any(), -                eq(Settings.Secure.NEARBY_SHARING_COMPONENT) -            ) -        ).thenReturn(anotherNearby?.flattenToString()) - -        components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) - -        assertThat(components.nearbySharingComponent).isEqualTo(anotherNearby) -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt deleted file mode 100644 index c8cb4b9b..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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 - -import android.content.ComponentName -import android.content.Intent -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ResolveInfoFlags -import android.os.UserHandle -import android.view.View -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.TextView -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.chooser.DisplayResolveInfo -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.internal.R -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@RunWith(AndroidJUnit4::class) -class ChooserListAdapterTest { -    private val userHandle: UserHandle = -        InstrumentationRegistry.getInstrumentation().targetContext.user - -    private val packageManager = -        mock<PackageManager> { -            whenever(resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(mock()) -        } -    private val context = InstrumentationRegistry.getInstrumentation().context -    private val resolverListController = mock<ResolverListController>() -    private val mEventLog = mock<EventLog>() -    private val mTargetDataLoader = mock<TargetDataLoader>() - -    private val testSubject by lazy { -        ChooserListAdapter( -            context, -            emptyList(), -            emptyArray(), -            emptyList(), -            false, -            resolverListController, -            userHandle, -            Intent(), -            mock(), -            packageManager, -            mEventLog, -            mock(), -            0, -            null, -            mTargetDataLoader -        ) -    } - -    @Before -    fun setup() { -        // ChooserListAdapter reads DeviceConfig and needs a permission for that. -        InstrumentationRegistry.getInstrumentation() -            .uiAutomation -            .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") -    } - -    @Test -    fun testDirectShareTargetLoadingIconIsStarted() { -        val view = createView() -        val viewHolder = ResolverListAdapter.ViewHolder(view) -        view.tag = viewHolder -        val targetInfo = createSelectableTargetInfo() -        testSubject.onBindView(view, targetInfo, 0) - -        verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) -    } - -    @Test -    fun onBindView_DirectShareTargetIconAndLabelLoadedOnlyOnce() { -        val view = createView() -        val viewHolderOne = ResolverListAdapter.ViewHolder(view) -        view.tag = viewHolderOne -        val targetInfo = createSelectableTargetInfo() -        testSubject.onBindView(view, targetInfo, 0) - -        val viewHolderTwo = ResolverListAdapter.ViewHolder(view) -        view.tag = viewHolderTwo - -        testSubject.onBindView(view, targetInfo, 0) - -        verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) -    } - -    @Test -    fun onBindView_AppTargetIconAndLabelLoadedOnlyOnce() { -        val view = createView() -        val viewHolderOne = ResolverListAdapter.ViewHolder(view) -        view.tag = viewHolderOne -        val targetInfo = -            DisplayResolveInfo.newDisplayResolveInfo( -                Intent(), -                ResolverDataProvider.createResolveInfo(2, 0, userHandle), -                null, -                "extended info", -                Intent(), -                /* resolveInfoPresentationGetter= */ null -            ) -        testSubject.onBindView(view, targetInfo, 0) - -        val viewHolderTwo = ResolverListAdapter.ViewHolder(view) -        view.tag = viewHolderTwo - -        testSubject.onBindView(view, targetInfo, 0) - -        verify(mTargetDataLoader, times(1)).loadAppTargetIcon(any(), any(), any()) -    } - -    private fun createSelectableTargetInfo(): TargetInfo = -        SelectableTargetInfo.newSelectableTargetInfo( -            /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo( -                Intent(), -                ResolverDataProvider.createResolveInfo(2, 0, userHandle), -                "label", -                "extended info", -                Intent(), -                /* resolveInfoPresentationGetter= */ null -            ), -            /* backupResolveInfo = */ mock(), -            /* resolvedIntent = */ Intent(), -            /* chooserTarget = */ createChooserTarget( -                "Target", -                0.5f, -                ComponentName("pkg", "Class"), -                "id-1" -            ), -            /* modifiedScore = */ 1f, -            /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1), -            /* appTarget */ null, -            /* referrerFillInIntent = */ Intent() -        ) - -    private fun createView(): View { -        val view = FrameLayout(context) -        TextView(context).apply { -            id = R.id.text1 -            view.addView(this) -        } -        TextView(context).apply { -            id = R.id.text2 -            view.addView(this) -        } -        ImageView(context).apply { -            id = R.id.icon -            view.addView(this) -        } -        return view -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt deleted file mode 100644 index 61ac0c21..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt +++ /dev/null @@ -1,242 +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 - -import android.app.Activity -import android.app.Application -import android.content.Intent -import android.content.IntentSender -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.os.ResultReceiver -import androidx.lifecycle.Observer -import androidx.test.annotation.UiThreadTest -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.intentresolver.ChooserRefinementManager.RefinementCompletion -import com.android.intentresolver.chooser.ImmutableTargetInfo -import com.android.intentresolver.chooser.TargetInfo -import com.google.common.truth.Truth.assertThat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Mockito - -@RunWith(AndroidJUnit4::class) -@UiThreadTest -class ChooserRefinementManagerTest { -    private val refinementManager = ChooserRefinementManager() -    private val intentSender = mock<IntentSender>() -    private val application = mock<Application>() -    private val exampleSourceIntents = -        listOf(Intent(Intent.ACTION_VIEW), Intent(Intent.ACTION_EDIT)) -    private val exampleTargetInfo = -        ImmutableTargetInfo.newBuilder().setAllSourceIntents(exampleSourceIntents).build() - -    private val completionObserver = -        object : Observer<RefinementCompletion> { -            val failureCountDown = CountDownLatch(1) -            val successCountDown = CountDownLatch(1) -            var latestTargetInfo: TargetInfo? = null - -            override fun onChanged(completion: RefinementCompletion) { -                if (completion.consume()) { -                    val targetInfo = completion.targetInfo -                    if (targetInfo == null) { -                        failureCountDown.countDown() -                    } else { -                        latestTargetInfo = targetInfo -                        successCountDown.countDown() -                    } -                } -            } -        } - -    /** Synchronously executes post() calls. */ -    private class FakeHandler(looper: Looper) : Handler(looper) { -        override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean { -            dispatchMessage(msg) -            return true -        } -    } - -    @Before -    fun setup() { -        refinementManager.refinementCompletion.observeForever(completionObserver) -    } - -    @Test -    fun testTypicalRefinementFlow() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    exampleTargetInfo, -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isTrue() - -        val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) -        Mockito.verify(intentSender) -            .sendIntent(any(), eq(0), intentCaptor.capture(), eq(null), eq(null)) - -        val intent = intentCaptor.value -        assertThat(intent?.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) -            .isEqualTo(exampleSourceIntents[0]) - -        val alternates = -            intent?.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java) -        assertThat(alternates?.size).isEqualTo(1) -        assertThat(alternates?.get(0)).isEqualTo(exampleSourceIntents[1]) - -        // Complete the refinement -        val receiver = -            intent?.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) -        val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } -        receiver?.send(Activity.RESULT_OK, bundle) - -        assertThat(completionObserver.successCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() -        assertThat(completionObserver.latestTargetInfo?.resolvedIntent?.action) -            .isEqualTo(Intent.ACTION_VIEW) -    } - -    @Test -    fun testRefinementCancelled() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    exampleTargetInfo, -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isTrue() - -        val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) -        Mockito.verify(intentSender) -            .sendIntent(any(), eq(0), intentCaptor.capture(), eq(null), eq(null)) - -        val intent = intentCaptor.value - -        // Complete the refinement -        val receiver = -            intent?.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) -        val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } -        receiver?.send(Activity.RESULT_CANCELED, bundle) - -        assertThat(completionObserver.failureCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() -    } - -    @Test -    fun testMaybeHandleSelection_noSourceIntents() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    ImmutableTargetInfo.newBuilder().build(), -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isFalse() -    } - -    @Test -    fun testMaybeHandleSelection_suspended() { -        val targetInfo = -            ImmutableTargetInfo.newBuilder() -                .setAllSourceIntents(exampleSourceIntents) -                .setIsSuspended(true) -                .build() - -        assertThat( -                refinementManager.maybeHandleSelection( -                    targetInfo, -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isFalse() -    } - -    @Test -    fun testMaybeHandleSelection_noIntentSender() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    exampleTargetInfo, -                    /* IntentSender */ null, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isFalse() -    } - -    @Test -    fun testConfigurationChangeDuringRefinement() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    exampleTargetInfo, -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())) -                ) -            ) -            .isTrue() - -        refinementManager.onActivityStop(/* config changing = */ true) -        refinementManager.onActivityResume() - -        assertThat(completionObserver.failureCountDown.count).isEqualTo(1) -    } - -    @Test -    fun testResumeDuringRefinement() { -        assertThat( -                refinementManager.maybeHandleSelection( -                    exampleTargetInfo, -                    intentSender, -                    application, -                    FakeHandler(checkNotNull(Looper.myLooper())!!) -                ) -            ) -            .isTrue() - -        refinementManager.onActivityStop(/* config changing = */ false) -        // Resume during refinement but not during a config change, so finish the activity. -        refinementManager.onActivityResume() - -        // Call should be synchronous, don't need to await for this one. -        assertThat(completionObserver.failureCountDown.count).isEqualTo(0) -    } - -    @Test -    fun testRefinementCompletion() { -        val refinementCompletion = RefinementCompletion(exampleTargetInfo) -        assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo) -        assertThat(refinementCompletion.consume()).isTrue() -        assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo) - -        // can only consume once. -        assertThat(refinementCompletion.consume()).isFalse() -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt b/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt deleted file mode 100644 index 331d1c21..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt +++ /dev/null @@ -1,88 +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 - -import android.app.PendingIntent -import android.content.Intent -import android.graphics.drawable.Icon -import android.net.Uri -import android.service.chooser.ChooserAction -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ChooserRequestParametersTest { -    val flags = TestFeatureFlagRepository(mapOf()) - -    @Test -    fun testChooserActions() { -        val actionCount = 3 -        val intent = Intent(Intent.ACTION_SEND) -        val actions = createChooserActions(actionCount) -        val chooserIntent = -            Intent(Intent.ACTION_CHOOSER).apply { -                putExtra(Intent.EXTRA_INTENT, intent) -                putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, actions) -            } -        val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) -        assertThat(request.chooserActions).containsExactlyElementsIn(actions).inOrder() -    } - -    @Test -    fun testChooserActions_empty() { -        val intent = Intent(Intent.ACTION_SEND) -        val chooserIntent = -            Intent(Intent.ACTION_CHOOSER).apply { putExtra(Intent.EXTRA_INTENT, intent) } -        val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) -        assertThat(request.chooserActions).isEmpty() -    } - -    @Test -    fun testChooserActions_tooMany() { -        val intent = Intent(Intent.ACTION_SEND) -        val chooserActions = createChooserActions(10) -        val chooserIntent = -            Intent(Intent.ACTION_CHOOSER).apply { -                putExtra(Intent.EXTRA_INTENT, intent) -                putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions) -            } - -        val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) - -        val expectedActions = chooserActions.sliceArray(0 until 5) -        assertThat(request.chooserActions).containsExactlyElementsIn(expectedActions).inOrder() -    } - -    private fun createChooserActions(count: Int): Array<ChooserAction> { -        return Array(count) { i -> createChooserAction("$i") } -    } - -    private fun createChooserAction(label: CharSequence): ChooserAction { -        val icon = Icon.createWithContentUri("content://org.package.app/image") -        val pendingIntent = -            PendingIntent.getBroadcast( -                InstrumentationRegistry.getInstrumentation().getTargetContext(), -                0, -                Intent("TESTACTION"), -                PendingIntent.FLAG_IMMUTABLE -            ) -        return ChooserAction.Builder(icon, label, pendingIntent).build() -    } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java deleted file mode 100644 index 8608cf72..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright (C) 2008 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.annotation.Nullable; -import android.app.prediction.AppPredictor; -import android.app.usage.UsageStatsManager; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.UserHandle; - -import androidx.lifecycle.ViewModelProvider; - -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.grid.ChooserGridAdapter; -import com.android.intentresolver.icons.TargetDataLoader; -import com.android.intentresolver.logging.EventLog; -import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; - -import java.util.List; -import java.util.function.Consumer; - -/** - * Simple wrapper around chooser activity to be able to initiate it under test. For more - * information, see {@code com.android.internal.app.ChooserWrapperActivity}. - */ -public class ChooserWrapperActivity -        extends com.android.intentresolver.ChooserActivity implements IChooserWrapper { -    static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance(); -    private UsageStatsManager mUsm; - -    // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at -    // onCreate and needs to see some non-negative value in the test. -    @Override -    public int getLaunchedFromUid() { -        return 1234; -    } - -    @Override -    public ChooserListAdapter createChooserListAdapter( -            Context context, -            List<Intent> payloadIntents, -            Intent[] initialIntents, -            List<ResolveInfo> rList, -            boolean filterLastUsed, -            ResolverListController resolverListController, -            UserHandle userHandle, -            Intent targetIntent, -            ChooserRequestParameters chooserRequest, -            int maxTargetsPerRow, -            TargetDataLoader targetDataLoader) { -        PackageManager packageManager = -                sOverrides.packageManager == null ? context.getPackageManager() -                        : sOverrides.packageManager; -        return new ChooserListAdapter( -                context, -                payloadIntents, -                initialIntents, -                rList, -                filterLastUsed, -                createListController(userHandle), -                userHandle, -                targetIntent, -                this, -                packageManager, -                getEventLog(), -                chooserRequest, -                maxTargetsPerRow, -                userHandle, -                targetDataLoader); -    } - -    @Override -    public ChooserListAdapter getAdapter() { -        return mChooserMultiProfilePagerAdapter.getActiveListAdapter(); -    } - -    @Override -    public ChooserListAdapter getPersonalListAdapter() { -        return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)) -                .getListAdapter(); -    } - -    @Override -    public ChooserListAdapter getWorkListAdapter() { -        if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { -            return null; -        } -        return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)) -                .getListAdapter(); -    } - -    @Override -    public boolean getIsSelected() { -        return mIsSuccessfullySelected; -    } - -    @Override -    protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { -        return new ChooserIntegratedDeviceComponents( -                /* editSharingComponent=*/ null, -                // An arbitrary pre-installed activity that handles this type of intent: -                /* nearbySharingComponent=*/ new ComponentName( -                        "com.google.android.apps.messaging", -                        ".ui.conversationlist.ShareIntentActivity")); -    } - -    @Override -    public UsageStatsManager getUsageStatsManager() { -        if (mUsm == null) { -            mUsm = getSystemService(UsageStatsManager.class); -        } -        return mUsm; -    } - -    @Override -    public boolean isVoiceInteraction() { -        if (sOverrides.isVoiceInteraction != null) { -            return sOverrides.isVoiceInteraction; -        } -        return super.isVoiceInteraction(); -    } - -    @Override -    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { -        if (sOverrides.mCrossProfileIntentsChecker != null) { -            return sOverrides.mCrossProfileIntentsChecker; -        } -        return super.createCrossProfileIntentsChecker(); -    } - -    @Override -    protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { -        if (sOverrides.mWorkProfileAvailability != null) { -            return sOverrides.mWorkProfileAvailability; -        } -        return super.createWorkProfileAvailabilityManager(); -    } - -    @Override -    public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, -            @Nullable Bundle options) { -        if (sOverrides.onSafelyStartInternalCallback != null -                && sOverrides.onSafelyStartInternalCallback.apply(cti)) { -            return; -        } -        super.safelyStartActivityInternal(cti, user, options); -    } - -    @Override -    protected ChooserListController createListController(UserHandle userHandle) { -        if (userHandle == UserHandle.SYSTEM) { -            return sOverrides.resolverListController; -        } -        return sOverrides.workResolverListController; -    } - -    @Override -    public PackageManager getPackageManager() { -        if (sOverrides.createPackageManager != null) { -            return sOverrides.createPackageManager.apply(super.getPackageManager()); -        } -        return super.getPackageManager(); -    } - -    @Override -    public Resources getResources() { -        if (sOverrides.resources != null) { -            return sOverrides.resources; -        } -        return super.getResources(); -    } - -    @Override -    protected ViewModelProvider.Factory createPreviewViewModelFactory() { -        return TestContentPreviewViewModel.Companion.wrap( -                super.createPreviewViewModelFactory(), -                sOverrides.imageLoader); -    } - -    @Override -    public EventLog getEventLog() { -        return sOverrides.mEventLog; -    } - -    @Override -    public Cursor queryResolver(ContentResolver resolver, Uri uri) { -        if (sOverrides.resolverCursor != null) { -            return sOverrides.resolverCursor; -        } - -        if (sOverrides.resolverForceException) { -            throw new SecurityException("Test exception handling"); -        } - -        return super.queryResolver(resolver, uri); -    } - -    @Override -    protected boolean isWorkProfile() { -        if (sOverrides.alternateProfileSetting != 0) { -            return sOverrides.alternateProfileSetting == MetricsEvent.MANAGED_PROFILE; -        } -        return super.isWorkProfile(); -    } - -    @Override -    public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, -            CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, -            @Nullable TargetPresentationGetter resolveInfoPresentationGetter) { -        return DisplayResolveInfo.newDisplayResolveInfo( -                originalIntent, -                pri, -                pLabel, -                pInfo, -                replacementIntent, -                resolveInfoPresentationGetter); -    } - -    @Override -    protected UserHandle getWorkProfileUserHandle() { -        return sOverrides.workProfileUserHandle; -    } - -    @Override -    public UserHandle getCurrentUserHandle() { -        return mMultiProfilePagerAdapter.getCurrentUserHandle(); -    } - -    @Override -    protected UserHandle getTabOwnerUserHandleForLaunch() { -        if (sOverrides.tabOwnerUserHandleForLaunch == null) { -            return super.getTabOwnerUserHandleForLaunch(); -        } -        return sOverrides.tabOwnerUserHandleForLaunch; -    } - -    @Override -    public Context createContextAsUser(UserHandle user, int flags) { -        // return the current context as a work profile doesn't really exist in these tests -        return getApplicationContext(); -    } - -    @Override -    protected ShortcutLoader createShortcutLoader( -            Context context, -            AppPredictor appPredictor, -            UserHandle userHandle, -            IntentFilter targetIntentFilter, -            Consumer<ShortcutLoader.Result> callback) { -        ShortcutLoader shortcutLoader = -                sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); -        if (shortcutLoader != null) { -            return shortcutLoader; -        } -        return super.createShortcutLoader( -                context, appPredictor, userHandle, targetIntentFilter, callback); -    } - -    @Override -    protected FeatureFlagRepository createFeatureFlagRepository() { -        if (sOverrides.featureFlagRepository != null) { -            return sOverrides.featureFlagRepository; -        } -        return super.createFeatureFlagRepository(); -    } -} diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt deleted file mode 100644 index c7d20000..00000000 --- a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt +++ /dev/null @@ -1,112 +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 - -import android.content.res.Resources -import android.view.View -import android.view.Window -import androidx.activity.ComponentActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.testing.TestLifecycleOwner -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -private const val TIMEOUT_MS = 200 - -@OptIn(ExperimentalCoroutinesApi::class) -class EnterTransitionAnimationDelegateTest { -    private val elementName = "shared-element" -    private val scheduler = TestCoroutineScheduler() -    private val dispatcher = StandardTestDispatcher(scheduler) -    private val lifecycleOwner = TestLifecycleOwner() - -    private val transitionTargetView = -        mock<View> { -            // avoid the request-layout path in the delegate -            whenever(isInLayout).thenReturn(true) -        } - -    private val windowMock = mock<Window>() -    private val resourcesMock = -        mock<Resources> { whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS) } -    private val activity = -        mock<ComponentActivity> { -            whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle) -            whenever(resources).thenReturn(resourcesMock) -            whenever(isActivityTransitionRunning).thenReturn(true) -            whenever(window).thenReturn(windowMock) -        } - -    private val testSubject = EnterTransitionAnimationDelegate(activity) { transitionTargetView } - -    @Before -    fun setup() { -        Dispatchers.setMain(dispatcher) -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) -    } - -    @After -    fun cleanup() { -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) -        Dispatchers.resetMain() -    } - -    @Test -    fun test_postponeTransition_timeout() { -        testSubject.postponeTransition() -        testSubject.markOffsetCalculated() - -        scheduler.advanceTimeBy(TIMEOUT_MS + 1L) -        verify(activity, times(1)).startPostponedEnterTransition() -        verify(windowMock, never()).setWindowAnimations(anyInt()) -    } - -    @Test -    fun test_postponeTransition_animation_resumes_only_once() { -        testSubject.postponeTransition() -        testSubject.markOffsetCalculated() -        testSubject.onTransitionElementReady(elementName) -        testSubject.markOffsetCalculated() -        testSubject.onTransitionElementReady(elementName) - -        scheduler.advanceTimeBy(TIMEOUT_MS + 1L) -        verify(activity, times(1)).startPostponedEnterTransition() -    } - -    @Test -    fun test_postponeTransition_resume_animation_conditions() { -        testSubject.postponeTransition() -        verify(activity, never()).startPostponedEnterTransition() - -        testSubject.markOffsetCalculated() -        verify(activity, never()).startPostponedEnterTransition() - -        testSubject.onAllTransitionElementsReady() -        verify(activity, times(1)).startPostponedEnterTransition() -    } -} diff --git a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt deleted file mode 100644 index 3fa01bcc..00000000 --- a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt +++ /dev/null @@ -1,56 +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 - -import com.android.systemui.flags.BooleanFlag -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Ignores tests annotated with [RequireFeatureFlags] which flag requirements does not - * meet in the active flag set. - * @param flags active flag set - */ -internal class FeatureFlagRule(flags: Map<BooleanFlag, Boolean>) : TestRule { -    private val flags = flags.entries.fold(HashMap<String, Boolean>()) { map, (key, value) -> -        map.apply { -            put(key.name, value) -        } -    } -    private val skippingStatement = object : Statement() { -        override fun evaluate() = Unit -    } - -    override fun apply(base: Statement, description: Description): Statement { -        val annotation = description.annotations.firstOrNull { -                it is RequireFeatureFlags -            } as? RequireFeatureFlags -            ?: return base - -        if (annotation.flags.size != annotation.values.size) { -            error("${description.className}#${description.methodName}: inconsistent number of" + -                    " flags and values in $annotation") -        } -        for (i in annotation.flags.indices) { -            val flag = annotation.flags[i] -            val value = annotation.values[i] -            if (flags.getOrDefault(flag, !value) != value) return skippingStatement -        } -        return base -    } -} diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java deleted file mode 100644 index 3326d7f2..00000000 --- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import android.annotation.Nullable; -import android.app.usage.UsageStatsManager; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.os.UserHandle; - -import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.logging.EventLog; - -import java.util.concurrent.Executor; - -/** - * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in - * order to expose the internals for override/inspection. Implementations should apply the overrides - * specified by the {@code ChooserActivityOverrideData} singleton. - */ -public interface IChooserWrapper { -    ChooserListAdapter getAdapter(); -    ChooserListAdapter getPersonalListAdapter(); -    ChooserListAdapter getWorkListAdapter(); -    boolean getIsSelected(); -    UsageStatsManager getUsageStatsManager(); -    DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, -            CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, -            @Nullable TargetPresentationGetter resolveInfoPresentationGetter); -    UserHandle getCurrentUserHandle(); -    EventLog getEventLog(); -    Executor getMainExecutor(); -} diff --git a/java/tests/src/com/android/intentresolver/MatcherUtils.java b/java/tests/src/com/android/intentresolver/MatcherUtils.java deleted file mode 100644 index 6168968b..00000000 --- a/java/tests/src/com/android/intentresolver/MatcherUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; - -/** - * Utils for helping with more customized matching options, for example matching the first - * occurrence of a set criteria. - */ -public class MatcherUtils { - -    /** -     * Returns a {@link Matcher} which only matches the first occurrence of a set criteria. -     */ -    static <T> Matcher<T> first(final Matcher<T> matcher) { -        return new BaseMatcher<T>() { -            boolean isFirstMatch = true; - -            @Override -            public boolean matches(final Object item) { -                if (isFirstMatch && matcher.matches(item)) { -                    isFirstMatch = false; -                    return true; -                } -                return false; -            } - -            @Override -            public void describeTo(final Description description) { -                description.appendText("Returns the first matching item"); -            } -        }; -    } -} diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt deleted file mode 100644 index aaa7a282..00000000 --- a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 - -/** - * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects - * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not - * be null"). To fix this, we can use methods that modify the return type to be nullable. This - * causes Kotlin to skip the null checks. - * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt - */ - -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatcher -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.mockito.stubbing.OngoingStubbing - -/** - * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -fun <T> eq(obj: T): T = Mockito.eq<T>(obj) - -/** - * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -fun <T> any(type: Class<T>): T = Mockito.any<T>(type) -inline fun <reified T> any(): T = any(T::class.java) - -/** - * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) - -/** - * Kotlin type-inferred version of Mockito.nullable() - */ -inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) - -/** - * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException - * when null is returned. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() - -/** - * Helper function for creating an argumentCaptor in kotlin. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = -    ArgumentCaptor.forClass(T::class.java) - -/** - * Helper function for creating new mocks, without the need to pass in a [Class] instance. - * - * Generic T is nullable because implicitly bounded by Any?. - * - * @param apply builder function to simplify stub configuration by improving type inference. - */ -inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) -    .apply(apply) - -/** - * Helper function for stubbing methods without the need to use backticks. - * - * @see Mockito.when - */ -fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall) - -/** - * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when - * kotlin tests are mocking kotlin objects and the methods take non-null parameters: - * - *     java.lang.NullPointerException: capture() must not be null - */ -class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { -    private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz) -    fun capture(): T = wrapped.capture() -    val value: T -        get() = wrapped.value -    val allValues: List<T> -        get() = wrapped.allValues -} - -/** - * Helper function for creating an argumentCaptor in kotlin. - * - * Generic T is nullable because implicitly bounded by Any?. - */ -inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = -    KotlinArgumentCaptor(T::class.java) - -/** - * Helper function for creating and using a single-use ArgumentCaptor in kotlin. - * - *    val captor = argumentCaptor<Foo>() - *    verify(...).someMethod(captor.capture()) - *    val captured = captor.value - * - * becomes: - * - *    val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } - * - * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. - */ -inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = -    kotlinArgumentCaptor<T>().apply { block() }.value - -/** - * Variant of [withArgCaptor] for capturing multiple arguments. - * - *    val captor = argumentCaptor<Foo>() - *    verify(...).someMethod(captor.capture()) - *    val captured: List<Foo> = captor.allValues - * - * becomes: - * - *    val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } - */ -inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = -    kotlinArgumentCaptor<T>().apply{ block() }.allValues - -inline fun <reified T> anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher<T?> { true }) diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java deleted file mode 100644 index 7233fd3d..00000000 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ /dev/null @@ -1,1100 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.swipeUp; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - -import static com.android.intentresolver.MatcherUtils.first; -import static com.android.intentresolver.ResolverWrapperActivity.sOverrides; - -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.RemoteException; -import android.os.UserHandle; -import android.text.TextUtils; -import android.view.View; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.test.InstrumentationRegistry; -import androidx.test.espresso.Espresso; -import androidx.test.espresso.NoMatchingViewException; -import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; - -import com.android.intentresolver.widget.ResolverDrawerLayout; - -import com.google.android.collect.Lists; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; - -import java.util.ArrayList; -import java.util.List; - -/** - * Resolver activity instrumentation tests - */ -@RunWith(AndroidJUnit4.class) -public class ResolverActivityTest { - -    private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app -            .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser(); -    protected Intent getConcreteIntentForLaunch(Intent clientIntent) { -        clientIntent.setClass( -                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(), -                ResolverWrapperActivity.class); -        return clientIntent; -    } - -    @Rule -    public ActivityTestRule<ResolverWrapperActivity> mActivityRule = -            new ActivityTestRule<>(ResolverWrapperActivity.class, false, false); - -    @Before -    public void setup() { -        // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the -        // permissions we require (which we'll read from the manifest at runtime). -        androidx.test.platform.app.InstrumentationRegistry -                .getInstrumentation() -                .getUiAutomation() -                .adoptShellPermissionIdentity(); - -        sOverrides.reset(); -    } - -    @Test -    public void twoOptionsAndUserSelectsOne() throws InterruptedException { -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, -                PERSONAL_USER_HANDLE); - -        setupResolverControllers(resolvedComponentInfos); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        assertThat(activity.getAdapter().getCount(), is(2)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); -        onView(withText(toChoose.activityInfo.name)) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Ignore // Failing - b/144929805 -    @Test -    public void setMaxHeight() throws Exception { -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, -                PERSONAL_USER_HANDLE); - -        setupResolverControllers(resolvedComponentInfos); -        waitForIdle(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); -        final int initialResolverHeight = viewPager.getHeight(); - -        activity.runOnUiThread(() -> { -            ResolverDrawerLayout layout = (ResolverDrawerLayout) -                    activity.findViewById( -                            com.android.internal.R.id.contentPanel); -            ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight -                    = initialResolverHeight - 1; -            // Force a relayout -            layout.invalidate(); -            layout.requestLayout(); -        }); -        waitForIdle(); -        assertThat("Drawer should be capped at maxHeight", -                viewPager.getHeight() == (initialResolverHeight - 1)); - -        activity.runOnUiThread(() -> { -            ResolverDrawerLayout layout = (ResolverDrawerLayout) -                    activity.findViewById( -                            com.android.internal.R.id.contentPanel); -            ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight -                    = initialResolverHeight + 1; -            // Force a relayout -            layout.invalidate(); -            layout.requestLayout(); -        }); -        waitForIdle(); -        assertThat("Drawer should not change height if its height is less than maxHeight", -                viewPager.getHeight() == initialResolverHeight); -    } - -    @Ignore // Failing - b/144929805 -    @Test -    public void setShowAtTopToTrue() throws Exception { -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, -                PERSONAL_USER_HANDLE); - -        setupResolverControllers(resolvedComponentInfos); -        waitForIdle(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); -        final View divider = activity.findViewById(com.android.internal.R.id.divider); -        final RelativeLayout profileView = -                (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button) -                        .getParent(); -        assertThat("Drawer should show at bottom by default", -                profileView.getBottom() + divider.getHeight() == viewPager.getTop() -                        && profileView.getTop() > 0); - -        activity.runOnUiThread(() -> { -            ResolverDrawerLayout layout = (ResolverDrawerLayout) -                    activity.findViewById( -                            com.android.internal.R.id.contentPanel); -            layout.setShowAtTop(true); -        }); -        waitForIdle(); -        assertThat("Drawer should show at top with new attribute", -                profileView.getBottom() + divider.getHeight() == viewPager.getTop() -                        && profileView.getTop() == 0); -    } - -    @Test -    public void hasLastChosenActivity() throws Exception { -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, -                PERSONAL_USER_HANDLE); -        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); - -        setupResolverControllers(resolvedComponentInfos); -        when(sOverrides.resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        // The other entry is filtered to the last used slot -        assertThat(activity.getAdapter().getCount(), is(1)); -        assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        onView(withId(com.android.internal.R.id.button_once)).perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test -    public void hasOtherProfileOneOption() throws Exception { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - -        ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); -        Intent sendIntent = createSendImageIntent(); -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        // The other entry is filtered to the last used slot -        assertThat(activity.getAdapter().getCount(), is(1)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10, -                        PERSONAL_USER_HANDLE); -        // We pick the first one as there is another one in the work profile side -        onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test -    public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); -        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - -        setupResolverControllers(resolvedComponentInfos); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        // The other entry is filtered to the other profile slot -        assertThat(activity.getAdapter().getCount(), is(2)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        // Confirm that the button bar is disabled by default -        onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); - -        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)).perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - - -    @Test -    public void hasLastChosenActivityAndOtherProfile() throws Exception { -        // In this case we prefer the other profile and don't display anything about the last -        // chosen activity. -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); -        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - -        setupResolverControllers(resolvedComponentInfos); -        when(sOverrides.resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        // The other entry is filtered to the other profile slot -        assertThat(activity.getAdapter().getCount(), is(2)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        // Confirm that the button bar is disabled by default -        onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); - -        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)).perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test -    public void testWorkTab_displayedWhenWorkProfileUserAvailable() { -        Intent sendIntent = createSendImageIntent(); -        markWorkProfileUserAvailable(); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { -        Intent sendIntent = createSendImageIntent(); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed()))); -    } - -    @Test -    public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10, -                        PERSONAL_USER_HANDLE); -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, -                new ArrayList<>(workResolvedComponentInfos)); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); -        // The work list adapter must be populated in advance before tapping the other tab -        assertThat(activity.getWorkListAdapter().getCount(), is(4)); -    } - -    @Test -    public void testWorkTab_workTabUsesExpectedAdapter() { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        markWorkProfileUserAvailable(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); - -        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); -        assertThat(activity.getWorkListAdapter().getCount(), is(4)); -    } - -    @Test -    public void testWorkTab_personalTabUsesExpectedAdapter() { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); - -        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); -        assertThat(activity.getPersonalListAdapter().getCount(), is(2)); -    } - -    @Test -    public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); -        waitForIdle(); -        assertThat(activity.getWorkListAdapter().getCount(), is(4)); -    } - -    @Test -    public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); -        waitForIdle(); -        onView(first(allOf(withText(workResolvedComponentInfos.get(0) -                .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); - -        waitForIdle(); -        assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); -    } - -    @Test -    public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets() -            throws InterruptedException { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); - -        waitForIdle(); -        assertThat(activity.getWorkListAdapter().getCount(), is(4)); -    } - -    @Test -    public void testWorkTab_headerIsVisibleInPersonalTab() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createOpenWebsiteIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        TextView headerText = activity.findViewById(com.android.internal.R.id.title); -        String initialText = headerText.getText().toString(); -        assertFalse("Header text is empty.", initialText.isEmpty()); -        assertThat(headerText.getVisibility(), is(View.VISIBLE)); -    } - -    @Test -    public void testWorkTab_switchTabs_headerStaysSame() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createOpenWebsiteIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        TextView headerText = activity.findViewById(com.android.internal.R.id.title); -        String initialText = headerText.getText().toString(); -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); - -        waitForIdle(); -        String currentText = headerText.getText().toString(); -        assertThat(headerText.getVisibility(), is(View.VISIBLE)); -        assertThat(String.format("Header text is not the same when switching tabs, personal profile" -                        + " header was %s but work profile header is %s", initialText, currentText), -                TextUtils.equals(initialText, currentText)); -    } - -    @Test -    public void testWorkTab_noPersonalApps_canStartWorkApps() -            throws InterruptedException { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); -        waitForIdle(); -        onView(first(allOf( -                withText(workResolvedComponentInfos.get(0) -                        .getResolveInfoAt(0).activityInfo.applicationInfo.name), -                isDisplayed()))) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); -        waitForIdle(); - -        assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); -    } - -    @Test -    public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets, -                        sOverrides.workProfileUserHandle); -        sOverrides.hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); - -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_workProfileDisabled_emptyStateShown() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets, -                        sOverrides.workProfileUserHandle); -        sOverrides.isQuietModeEnabled = true; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_turn_on_work_apps)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_no_work_apps_available)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); -        sOverrides.isQuietModeEnabled = true; -        sOverrides.hasCrossProfileIntents = false; - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testMiniResolver() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); -        // Personal profile only has a browser -        personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed())); -    } - -    @Test -    public void testMiniResolver_noCurrentProfileTarget() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        // Need to ensure mini resolver doesn't trigger here. -        assertNotMiniResolver(); -    } - -    private void assertNotMiniResolver() { -        try { -            onView(withId(com.android.internal.R.id.open_cross_profile)) -                    .check(matches(isDisplayed())); -        } catch (NoMatchingViewException e) { -            return; -        } -        fail("Mini resolver present but shouldn't be"); -    } - -    @Test -    public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); -        sOverrides.isQuietModeEnabled = true; - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_no_work_apps_available)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, -                        PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets, -                        sOverrides.workProfileUserHandle); -        sOverrides.hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            chosen[0] = result.first.getResolveInfo(); -            return true; -        }; - -        mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        assertNull(chosen[0]); -    } - -    @Test -    public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException { -        markWorkProfileUserAvailable(); - -        // In this case we prefer the other profile and don't display anything about the last -        // chosen activity. -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE); - -        setupResolverControllers(resolvedComponentInfos); -        when(sOverrides.resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        // The other entry is filtered to the last used slot -        assertThat(activity.getAdapter().hasFilteredItem(), is(false)); -        assertThat(activity.getAdapter().getCount(), is(2)); -        assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); -    } - -    @Test -    public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); -        setupResolverControllers(resolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); -        assertThat(activity.getAdapter().getCount(), is(3)); -    } - -    @Test -    public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { -        markWorkProfileUserAvailable(); -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, -                sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); -        assertThat(activity.getAdapter().getCount(), is(3)); -    } - -    @Test -    public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception { -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        2, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); - -        setupResolverControllers(resolvedComponentInfos); -        when(sOverrides.resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        Espresso.registerIdlingResources(activity.getLabelIdlingResource()); -        waitForIdle(); - -        assertThat(activity.getAdapter().hasFilteredItem(), is(false)); -        assertThat(activity.getAdapter().getCount(), is(2)); -        assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); -    } - -    @Test -    public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception { -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        Intent sendIntent = createSendImageIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); - -        setupResolverControllers(resolvedComponentInfos); -        when(sOverrides.resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); - -        // Confirm that the button bar is disabled by default -        onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); -        onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); - -        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); - -        onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled())); -        onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); -    } - -    @Test -    public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser() -            throws Exception { -        markWorkProfileUserAvailable(); -        // enable cloneProfile -        markCloneProfileUserAvailable(); - -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); -        sOverrides.hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); -        final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            selectedActivityUserHandle[0] = result.second; -            return true; -        }; - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(first(allOf(withText(personalResolvedComponentInfos.get(0) -                .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); -        waitForIdle(); - -        assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); -    } - -    @Test -    public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser() -            throws Exception { -        markWorkProfileUserAvailable(); -        // enable cloneProfile -        markCloneProfileUserAvailable(); - -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); -        sendIntent.setType("TestType"); -        final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; -        sOverrides.onSafelyStartInternalCallback = result -> { -            selectedActivityUserHandle[0] = result.second; -            return true; -        }; - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)) -                .perform(click()); -        waitForIdle(); -        onView(first(allOf(withText(workResolvedComponentInfos.get(0) -                .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) -                .perform(click()); -        onView(withId(com.android.internal.R.id.button_once)) -                .perform(click()); -        waitForIdle(); - -        assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); -    } - -    @Test -    public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers() -            throws Exception { -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        sOverrides.cloneProfileUserHandle); -        setupResolverControllers(resolvedComponentInfos); -        Intent sendIntent = createSendImageIntent(); - -        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); -        waitForIdle(); -        List<UserHandle> result = activity -                .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE); - -        assertThat(result.containsAll(Lists.newArrayList(PERSONAL_USER_HANDLE, -                sOverrides.cloneProfileUserHandle)), is(true)); -    } - -    private Intent createSendImageIntent() { -        Intent sendIntent = new Intent(); -        sendIntent.setAction(Intent.ACTION_SEND); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); -        sendIntent.setType("image/jpeg"); -        return sendIntent; -    } - -    private Intent createOpenWebsiteIntent() { -        Intent sendIntent = new Intent(); -        sendIntent.setAction(Intent.ACTION_VIEW); -        sendIntent.setData(Uri.parse("https://google.com")); -        return sendIntent; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, -            UserHandle resolvedForUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( -            int numberOfResults, -            UserHandle resolvedForPersonalUser, -            UserHandle resolvedForClonedUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < 1; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                    resolvedForPersonalUser)); -        } -        for (int i = 1; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                    resolvedForClonedUser)); -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( -            int numberOfResults, -            UserHandle resolvedForUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            if (i == 0) { -                infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, -                        resolvedForUser)); -            } else { -                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); -            } -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( -            int numberOfResults, int userId, UserHandle resolvedForUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            if (i == 0) { -                infoList.add( -                        ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, -                                resolvedForUser)); -            } else { -                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); -            } -        } -        return infoList; -    } - -    private void waitForIdle() { -        InstrumentationRegistry.getInstrumentation().waitForIdleSync(); -    } - -    private void markWorkProfileUserAvailable() { -        ResolverWrapperActivity.sOverrides.workProfileUserHandle = UserHandle.of(10); -    } - -    private void setupResolverControllers( -            List<ResolvedComponentInfo> personalResolvedComponentInfos) { -        setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); -    } - -    private void markCloneProfileUserAvailable() { -        ResolverWrapperActivity.sOverrides.cloneProfileUserHandle = UserHandle.of(11); -    } - -    private void setupResolverControllers( -            List<ResolvedComponentInfo> personalResolvedComponentInfos, -            List<ResolvedComponentInfo> workResolvedComponentInfos) { -        when(sOverrides.resolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(UserHandle.SYSTEM))) -                        .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when(sOverrides.workResolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(UserHandle.SYSTEM))) -                        .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when(sOverrides.workResolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(UserHandle.of(10)))) -                        .thenReturn(new ArrayList<>(workResolvedComponentInfos)); -    } -} diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java deleted file mode 100644 index 1f8d9bee..00000000 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (C) 2008 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.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.os.UserHandle; -import android.test.mock.MockContext; -import android.test.mock.MockPackageManager; -import android.test.mock.MockResources; - -/** - * Utility class used by resolver tests to create mock data - */ -public class ResolverDataProvider { - -    static private int USER_SOMEONE_ELSE = 10; - -    static ResolvedComponentInfo createResolvedComponentInfo(int i) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, UserHandle.USER_CURRENT)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfo(int i, -            UserHandle resolvedForUser) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, UserHandle.USER_CURRENT, resolvedForUser)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfo( -            ComponentName componentName, Intent intent) { -        return new ResolvedComponentInfo( -                componentName, -                intent, -                createResolveInfo(componentName, UserHandle.USER_CURRENT)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfo( -            ComponentName componentName, Intent intent, UserHandle resolvedForUser) { -        return new ResolvedComponentInfo( -                componentName, -                intent, -                createResolveInfo(componentName, UserHandle.USER_CURRENT, resolvedForUser)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, USER_SOMEONE_ELSE)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, -            UserHandle resolvedForUser) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, USER_SOMEONE_ELSE, resolvedForUser)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, userId)); -    } - -    static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, -            int userId, UserHandle resolvedForUser) { -        return new ResolvedComponentInfo( -                createComponentName(i), -                createResolverIntent(i), -                createResolveInfo(i, userId, resolvedForUser)); -    } - -    public static ComponentName createComponentName(int i) { -        final String name = "component" + i; -        return new ComponentName("foo.bar." + name, name); -    } - -    public static ResolveInfo createResolveInfo(int i, int userId) { -        return createResolveInfo(i, userId, UserHandle.of(userId)); -    } - -    public static ResolveInfo createResolveInfo(int i, int userId, UserHandle resolvedForUser) { -        return createResolveInfo(createActivityInfo(i), userId, resolvedForUser); -    } - -    public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) { -        return createResolveInfo(componentName, userId, UserHandle.of(userId)); -    } - -    public static ResolveInfo createResolveInfo( -            ComponentName componentName, int userId, UserHandle resolvedForUser) { -        return createResolveInfo(createActivityInfo(componentName), userId, resolvedForUser); -    } - -    public static ResolveInfo createResolveInfo( -            ActivityInfo activityInfo, int userId, UserHandle resolvedForUser) { -        final ResolveInfo resolveInfo = new ResolveInfo(); -        resolveInfo.activityInfo = activityInfo; -        resolveInfo.targetUserId = userId; -        resolveInfo.userHandle = resolvedForUser; -        return resolveInfo; -    } - -    static ActivityInfo createActivityInfo(int i) { -        ActivityInfo ai = new ActivityInfo(); -        ai.name = "activity_name" + i; -        ai.packageName = "foo_bar" + i; -        ai.enabled = true; -        ai.exported = true; -        ai.permission = null; -        ai.applicationInfo = createApplicationInfo(); -        return ai; -    } - -    static ActivityInfo createActivityInfo(ComponentName componentName) { -        ActivityInfo ai = new ActivityInfo(); -        ai.name = componentName.getClassName(); -        ai.packageName = componentName.getPackageName(); -        ai.enabled = true; -        ai.exported = true; -        ai.permission = null; -        ai.applicationInfo = createApplicationInfo(); -        ai.applicationInfo.packageName = componentName.getPackageName(); -        return ai; -    } - -    static ApplicationInfo createApplicationInfo() { -        ApplicationInfo ai = new ApplicationInfo(); -        ai.name = "app_name"; -        ai.packageName = "foo.bar"; -        ai.enabled = true; -        return ai; -    } - -    static class PackageManagerMockedInfo { -        public Context ctx; -        public ApplicationInfo appInfo; -        public ActivityInfo activityInfo; -        public ResolveInfo resolveInfo; -        public String setAppLabel; -        public String setActivityLabel; -        public String setResolveInfoLabel; -    } - -    /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */ -    static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) { -        return createPackageManagerMockedInfo( -                hasOverridePermission, "app_label", "activity_label", "resolve_info_label"); -    } - -    static PackageManagerMockedInfo createPackageManagerMockedInfo( -            boolean hasOverridePermission, -            String appLabel, -            String activityLabel, -            String resolveInfoLabel) { -        MockContext ctx = new MockContext() { -            @Override -            public PackageManager getPackageManager() { -                return new MockPackageManager() { -                    @Override -                    public int checkPermission(String permName, String pkgName) { -                        if (hasOverridePermission) return PERMISSION_GRANTED; -                        return PERMISSION_DENIED; -                    } -                }; -            } - -            @Override -            public Resources getResources() { -                return new MockResources() { -                    @Override -                    public String getString(int id) throws NotFoundException { -                        if (id == 1) return appLabel; -                        if (id == 2) return activityLabel; -                        if (id == 3) return resolveInfoLabel; -                        return null; -                    } -                }; -            } -        }; - -        ApplicationInfo appInfo = new ApplicationInfo() { -            @Override -            public CharSequence loadLabel(PackageManager pm) { -                return appLabel; -            } -        }; -        appInfo.labelRes = 1; - -        ActivityInfo activityInfo = new ActivityInfo() { -            @Override -            public CharSequence loadLabel(PackageManager pm) { -                return activityLabel; -            } -        }; -        activityInfo.labelRes = 2; -        activityInfo.applicationInfo = appInfo; - -        ResolveInfo resolveInfo = new ResolveInfo() { -            @Override -            public CharSequence loadLabel(PackageManager pm) { -                return resolveInfoLabel; -            } -        }; -        resolveInfo.activityInfo = activityInfo; -        resolveInfo.resolvePackageName = "super.fake.packagename"; -        resolveInfo.labelRes = 3; - -        PackageManagerMockedInfo mockedInfo = new PackageManagerMockedInfo(); -        mockedInfo.activityInfo = activityInfo; -        mockedInfo.appInfo = appInfo; -        mockedInfo.ctx = ctx; -        mockedInfo.resolveInfo = resolveInfo; -        mockedInfo.setAppLabel = appLabel; -        mockedInfo.setActivityLabel = activityLabel; -        mockedInfo.setResolveInfoLabel = resolveInfoLabel; - -        return mockedInfo; -    } - -    static Intent createResolverIntent(int i) { -        return new Intent("intentAction" + i); -    } -} diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java deleted file mode 100644 index 401ede26..00000000 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.annotation.Nullable; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.UserHandle; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.test.espresso.idling.CountingIdlingResource; - -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.chooser.SelectableTargetInfo; -import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.icons.TargetDataLoader; - -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; - -/* - * Simple wrapper around chooser activity to be able to initiate it under test - */ -public class ResolverWrapperActivity extends ResolverActivity { -    static final OverrideData sOverrides = new OverrideData(); - -    private final CountingIdlingResource mLabelIdlingResource = -            new CountingIdlingResource("LoadLabelTask"); - -    public ResolverWrapperActivity() { -        super(/* isIntentPicker= */ true); -    } - -    // ResolverActivity inspects the launched-from UID at onCreate and needs to see some -    // non-negative value in the test. -    @Override -    public int getLaunchedFromUid() { -        return 1234; -    } - -    public CountingIdlingResource getLabelIdlingResource() { -        return mLabelIdlingResource; -    } - -    @Override -    public ResolverListAdapter createResolverListAdapter( -            Context context, -            List<Intent> payloadIntents, -            Intent[] initialIntents, -            List<ResolveInfo> rList, -            boolean filterLastUsed, -            UserHandle userHandle, -            TargetDataLoader targetDataLoader) { -        return new ResolverListAdapter( -                context, -                payloadIntents, -                initialIntents, -                rList, -                filterLastUsed, -                createListController(userHandle), -                userHandle, -                payloadIntents.get(0),  // TODO: extract upstream -                this, -                userHandle, -                new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource)); -    } - -    @Override -    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { -        if (sOverrides.mCrossProfileIntentsChecker != null) { -            return sOverrides.mCrossProfileIntentsChecker; -        } -        return super.createCrossProfileIntentsChecker(); -    } - -    @Override -    protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { -        if (sOverrides.mWorkProfileAvailability != null) { -            return sOverrides.mWorkProfileAvailability; -        } -        return super.createWorkProfileAvailabilityManager(); -    } - -    ResolverListAdapter getAdapter() { -        return mMultiProfilePagerAdapter.getActiveListAdapter(); -    } - -    ResolverListAdapter getPersonalListAdapter() { -        return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)); -    } - -    ResolverListAdapter getWorkListAdapter() { -        if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { -            return null; -        } -        return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)); -    } - -    @Override -    public boolean isVoiceInteraction() { -        if (sOverrides.isVoiceInteraction != null) { -            return sOverrides.isVoiceInteraction; -        } -        return super.isVoiceInteraction(); -    } - -    @Override -    public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, -            @Nullable Bundle options) { -        if (sOverrides.onSafelyStartInternalCallback != null -                && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) { -            return; -        } -        super.safelyStartActivityInternal(cti, user, options); -    } - -    @Override -    protected ResolverListController createListController(UserHandle userHandle) { -        if (userHandle == UserHandle.SYSTEM) { -            return sOverrides.resolverListController; -        } -        return sOverrides.workResolverListController; -    } - -    @Override -    public PackageManager getPackageManager() { -        if (sOverrides.createPackageManager != null) { -            return sOverrides.createPackageManager.apply(super.getPackageManager()); -        } -        return super.getPackageManager(); -    } - -    protected UserHandle getCurrentUserHandle() { -        return mMultiProfilePagerAdapter.getCurrentUserHandle(); -    } - -    @Override -    protected UserHandle getWorkProfileUserHandle() { -        return sOverrides.workProfileUserHandle; -    } - -    @Override -    protected UserHandle getCloneProfileUserHandle() { -        return sOverrides.cloneProfileUserHandle; -    } - -    @Override -    public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { -        super.startActivityAsUser(intent, options, user); -    } - -    @Override -    protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle -            userHandle) { -        return super.getResolverRankerServiceUserHandleListInternal(userHandle); -    } - -    /** -     * We cannot directly mock the activity created since instrumentation creates it. -     * <p> -     * Instead, we use static instances of this object to modify behavior. -     */ -    static class OverrideData { -        @SuppressWarnings("Since15") -        public Function<PackageManager, PackageManager> createPackageManager; -        public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback; -        public ResolverListController resolverListController; -        public ResolverListController workResolverListController; -        public Boolean isVoiceInteraction; -        public UserHandle workProfileUserHandle; -        public UserHandle cloneProfileUserHandle; -        public UserHandle tabOwnerUserHandleForLaunch; -        public Integer myUserId; -        public boolean hasCrossProfileIntents; -        public boolean isQuietModeEnabled; -        public WorkProfileAvailabilityManager mWorkProfileAvailability; -        public CrossProfileIntentsChecker mCrossProfileIntentsChecker; - -        public void reset() { -            onSafelyStartInternalCallback = null; -            isVoiceInteraction = null; -            createPackageManager = null; -            resolverListController = mock(ResolverListController.class); -            workResolverListController = mock(ResolverListController.class); -            workProfileUserHandle = null; -            cloneProfileUserHandle = null; -            tabOwnerUserHandleForLaunch = null; -            myUserId = null; -            hasCrossProfileIntents = true; -            isQuietModeEnabled = false; - -            mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { -                @Override -                public boolean isQuietModeEnabled() { -                    return isQuietModeEnabled; -                } - -                @Override -                public boolean isWorkProfileUserUnlocked() { -                    return true; -                } - -                @Override -                public void requestQuietModeEnabled(boolean enabled) { -                    isQuietModeEnabled = enabled; -                } - -                @Override -                public void markWorkProfileEnabledBroadcastReceived() {} - -                @Override -                public boolean isWaitingToEnableWorkProfile() { -                    return false; -                } -            }; - -            mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); -            when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) -                    .thenAnswer(invocation -> hasCrossProfileIntents); -        } -    } - -    private static class TargetDataLoaderWrapper extends TargetDataLoader { -        private final TargetDataLoader mTargetDataLoader; -        private final CountingIdlingResource mLabelIdlingResource; - -        private TargetDataLoaderWrapper( -                TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) { -            mTargetDataLoader = targetDataLoader; -            mLabelIdlingResource = labelIdlingResource; -        } - -        @Override -        public void loadAppTargetIcon( -                @NonNull DisplayResolveInfo info, -                @NonNull UserHandle userHandle, -                @NonNull Consumer<Drawable> callback) { -            mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); -        } - -        @Override -        public void loadDirectShareIcon( -                @NonNull SelectableTargetInfo info, -                @NonNull UserHandle userHandle, -                @NonNull Consumer<Drawable> callback) { -            mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback); -        } - -        @Override -        public void loadLabel( -                @NonNull DisplayResolveInfo info, -                @NonNull Consumer<CharSequence[]> callback) { -            mLabelIdlingResource.increment(); -            mTargetDataLoader.loadLabel( -                    info, -                    (result) -> { -                        mLabelIdlingResource.decrement(); -                        callback.accept(result); -                    }); -        } - -        @NonNull -        @Override -        public TargetPresentationGetter createPresentationGetter(@NonNull ResolveInfo info) { -            return mTargetDataLoader.createPresentationGetter(info); -        } -    } -} diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt deleted file mode 100644 index 9ddeed84..00000000 --- a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt +++ /dev/null @@ -1,313 +0,0 @@ -/* - * 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 - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.ResolveInfo -import android.content.pm.ShortcutInfo -import android.os.UserHandle -import android.service.chooser.ChooserTarget -import com.android.intentresolver.chooser.DisplayResolveInfo -import com.android.intentresolver.chooser.TargetInfo -import androidx.test.filters.SmallTest -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -private const val PACKAGE_A = "package.a" -private const val PACKAGE_B = "package.b" -private const val CLASS_NAME = "./MainActivity" - -@SmallTest -class ShortcutSelectionLogicTest { -    private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry -            .getInstrumentation().getTargetContext().getUser() - -    private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply { -        arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg -> -            // shortcuts in reverse priority order -            val targets = Array(3) { i -> -                createChooserTarget( -                    "Shortcut $i", -                    (i + 1).toFloat() / 10f, -                    ComponentName(pkg, CLASS_NAME), -                    pkg.shortcutId(i), -                ) -            } -            this[pkg] = targets -        } -    } - -    private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( -            Intent(), -            ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), -            "label", -            "extended info", -            Intent(), -            /* resolveInfoPresentationGetter= */ null) - -    private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( -            Intent(), -            ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE), -            "label 2", -            "extended info 2", -            Intent(), -            /* resolveInfoPresentationGetter= */ null) - -    private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) = -        this[pkg]?.get(idx) ?: error("missing package $pkg") - -    @Test -    fun testAddShortcuts_no_limits() { -        val serviceResults = ArrayList<TargetInfo>() -        val sc1 = packageTargets[PACKAGE_A, 0] -        val sc2 = packageTargets[PACKAGE_A, 1] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ false -        ) - -        val isUpdated = testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0.1f, -            /* targets = */ listOf(sc1, sc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) - -        assertTrue("Updates are expected", isUpdated) -        assertShortcutsInOrder( -            listOf(sc2, sc1), -            serviceResults, -            "Two shortcuts are expected as we do not apply per-app shortcut limit" -        ) -    } - -    @Test -    fun testAddShortcuts_same_package_with_per_package_limit() { -        val serviceResults = ArrayList<TargetInfo>() -        val sc1 = packageTargets[PACKAGE_A, 0] -        val sc2 = packageTargets[PACKAGE_A, 1] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ true -        ) - -        val isUpdated = testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0.1f, -            /* targets = */ listOf(sc1, sc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) - -        assertTrue("Updates are expected", isUpdated) -        assertShortcutsInOrder( -            listOf(sc2), -            serviceResults, -            "One shortcut is expected as we apply per-app shortcut limit" -        ) -    } - -    @Test -    fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() { -        val serviceResults = ArrayList<TargetInfo>() -        val sc1 = packageTargets[PACKAGE_A, 0] -        val sc2 = packageTargets[PACKAGE_A, 1] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ false -        ) - -        val isUpdated = testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0.1f, -            /* targets = */ listOf(sc1, sc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 1, -            /* serviceTargets = */ serviceResults -        ) - -        assertTrue("Updates are expected", isUpdated) -        assertShortcutsInOrder( -            listOf(sc2), -            serviceResults, -            "One shortcut is expected as we apply overall shortcut limit" -        ) -    } - -    @Test -    fun testAddShortcuts_different_packages_with_per_package_limit() { -        val serviceResults = ArrayList<TargetInfo>() -        val pkgAsc1 = packageTargets[PACKAGE_A, 0] -        val pkgAsc2 = packageTargets[PACKAGE_A, 1] -        val pkgBsc1 = packageTargets[PACKAGE_B, 0] -        val pkgBsc2 = packageTargets[PACKAGE_B, 1] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ true -        ) - -        testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0.1f, -            /* targets = */ listOf(pkgAsc1, pkgAsc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) -        testSubject.addServiceResults( -            /* origTarget = */ otherBaseDisplayInfo, -            /* origTargetScore = */ 0.2f, -            /* targets = */ listOf(pkgBsc1, pkgBsc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) - -        assertShortcutsInOrder( -            listOf(pkgBsc2, pkgAsc2), -            serviceResults, -            "Two shortcuts are expected as we apply per-app shortcut limit" -        ) -    } - -    @Test -    fun testAddShortcuts_pinned_shortcut() { -        val serviceResults = ArrayList<TargetInfo>() -        val sc1 = packageTargets[PACKAGE_A, 0] -        val sc2 = packageTargets[PACKAGE_A, 1] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ false -        ) - -        val isUpdated = testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0.1f, -            /* targets = */ listOf(sc1, sc2), -            /* isShortcutResult = */ true, -            /* directShareToShortcutInfos = */ mapOf( -                sc1 to createShortcutInfo( -                    PACKAGE_A.shortcutId(1), -                    sc1.componentName, 1).apply { -                        addFlags(ShortcutInfo.FLAG_PINNED) -                    } -            ), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ mock(), -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) - -        assertTrue("Updates are expected", isUpdated) -        assertShortcutsInOrder( -            listOf(sc1, sc2), -            serviceResults, -            "Two shortcuts are expected as we do not apply per-app shortcut limit" -        ) -    } - -    @Test -    fun test_available_caller_shortcuts_count_is_limited() { -        val serviceResults = ArrayList<TargetInfo>() -        val sc1 = packageTargets[PACKAGE_A, 0] -        val sc2 = packageTargets[PACKAGE_A, 1] -        val sc3 = packageTargets[PACKAGE_A, 2] -        val testSubject = ShortcutSelectionLogic( -            /* maxShortcutTargetsPerApp = */ 1, -            /* applySharingAppLimits = */ true -        ) -        val context = mock<Context> { -            whenever(packageManager).thenReturn(mock()) -        } - -        testSubject.addServiceResults( -            /* origTarget = */ baseDisplayInfo, -            /* origTargetScore = */ 0f, -            /* targets = */ listOf(sc1, sc2, sc3), -            /* isShortcutResult = */ false, -            /* directShareToShortcutInfos = */ emptyMap(), -            /* directShareToAppTargets = */ emptyMap(), -            /* userContext = */ context, -            /* targetIntent = */ mock(), -            /* refererFillInIntent = */ mock(), -            /* maxRankedTargets = */ 4, -            /* serviceTargets = */ serviceResults -        ) - -        assertShortcutsInOrder( -            listOf(sc3, sc2), -            serviceResults, -            "At most two caller-provided shortcuts are allowed" -        ) -    } - -    // TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases -    // add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`. -    private fun assertShortcutsInOrder( -        expected: List<ChooserTarget>, actual: List<TargetInfo>, msg: String? = "" -    ) { -        assertEquals(msg, expected.size, actual.size) -        for (i in expected.indices) { -            assertEquals( -                "Unexpected item at position $i", -                expected[i].componentName, -                actual[i].chooserTargetComponentName -            ) -            assertEquals( -                "Unexpected item at position $i", -                expected[i].title, -                actual[i].displayLabel -            ) -        } -    } - -    private fun String.shortcutId(id: Int) = "$this.$id" -} diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt deleted file mode 100644 index e62672a3..00000000 --- a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 - -import com.android.intentresolver.ResolverDataProvider -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -/** - * Unit tests for the various implementations of {@link TargetPresentationGetter}. - * TODO: consider expanding to cover icon logic (not just labels/sublabels). - * TODO: these are conceptually "acceptance tests" that provide comprehensive coverage of the - * apparent variations in the legacy implementation. The tests probably don't have to be so - * exhaustive if we're able to impose a simpler design on the implementation. - */ -class TargetPresentationGetterTest { -  fun makeResolveInfoPresentationGetter( -          withSubstitutePermission: Boolean, -          appLabel: String, -          activityLabel: String, -          resolveInfoLabel: String): TargetPresentationGetter { -      val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( -              withSubstitutePermission, appLabel, activityLabel, resolveInfoLabel) -      val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) -      return factory.makePresentationGetter(testPackageInfo.resolveInfo) -  } - -  fun makeActivityInfoPresentationGetter( -          withSubstitutePermission: Boolean, -          appLabel: String?, -          activityLabel: String?): TargetPresentationGetter { -      val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( -              withSubstitutePermission, appLabel, activityLabel, "") -      val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) -      return factory.makePresentationGetter(testPackageInfo.activityInfo) -  } - -  @Test -  fun testActivityInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter( -              false, "app_label", "activity_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") -  } - -  @Test -  fun testActivityInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter( -              false, "app_label", "app_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // Without the substitute permission, there's no logic to dedupe the labels. -      // TODO: this matches our observations in the legacy code, but is it the right behavior? It -      // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in -      // the UI at least, but maybe that logic should be pulled back to the "presentation"? -      assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") -  } - -  @Test -  fun testActivityInfoLabels_noSubstitutePermission_nullRequestedLabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(false, null, "activity_label") -      assertThat(presentationGetter.getLabel()).isNull() -      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") -  } - -  @Test -  fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedLabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(false, "", "activity_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("") -      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") -  } - -  @Test -  fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(false, "app_label", "") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // Without the substitute permission, empty sublabels are passed through as-is. -      assertThat(presentationGetter.getSubLabel()).isEqualTo("") -  } - -  @Test -  fun testActivityInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter( -              true, "app_label", "activity_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") -      // With the substitute permission, the same ("activity") label is requested as both the label -      // and sublabel, even though the other value ("app_label") was distinct. Thus this behaves the -      // same as a dupe. -      assertThat(presentationGetter.getSubLabel()).isEqualTo(null) -  } - -  @Test -  fun testActivityInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter( -              true, "app_label", "app_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // With the substitute permission, duped sublabels get converted to nulls. -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testActivityInfoLabels_withSubstitutePermission_nullRequestedLabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", null) -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // With the substitute permission, null inputs are a special case that produces null outputs -      // (i.e., they're not simply passed-through from the inputs). -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedLabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", "") -      // Empty "labels" are taken as-is and (unlike nulls) don't prompt a fallback to the sublabel. -      // Thus (as in the previous case with substitute permission & "distinct" labels), this is -      // treated as a dupe. -      assertThat(presentationGetter.getLabel()).isEqualTo("") -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { -      val presentationGetter = makeActivityInfoPresentationGetter(true, "", "activity_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") -      // With the substitute permission, empty sublabels get converted to nulls. -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testResolveInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              false, "app_label", "activity_label", "resolve_info_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") -  } - -  @Test -  fun testResolveInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              false, "app_label", "activity_label", "app_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // Without the substitute permission, there's no logic to dedupe the labels. -      // TODO: this matches our observations in the legacy code, but is it the right behavior? It -      // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in -      // the UI at least, but maybe that logic should be pulled back to the "presentation"? -      assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") -  } - -  @Test -  fun testResolveInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              false, "app_label", "activity_label", "") -      assertThat(presentationGetter.getLabel()).isEqualTo("app_label") -      // Without the substitute permission, empty sublabels are passed through as-is. -      assertThat(presentationGetter.getSubLabel()).isEqualTo("") -  } - -  @Test -  fun testResolveInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              true, "app_label", "activity_label", "resolve_info_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") -      assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") -  } - -  @Test -  fun testResolveInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              true, "app_label", "activity_label", "activity_label") -      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") -      // With the substitute permission, duped sublabels get converted to nulls. -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              true, "app_label", "activity_label", "") -      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") -      // With the substitute permission, empty sublabels get converted to nulls. -      assertThat(presentationGetter.getSubLabel()).isNull() -  } - -  @Test -  fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedLabelAndSublabel() { -      val presentationGetter = makeResolveInfoPresentationGetter( -              true, "app_label", "", "") -      assertThat(presentationGetter.getLabel()).isEqualTo("") -      // With the substitute permission, empty sublabels get converted to nulls. -      assertThat(presentationGetter.getSubLabel()).isNull() -  } -} diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt deleted file mode 100644 index 849cfbab..00000000 --- a/java/tests/src/com/android/intentresolver/TestApplication.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 - -import android.app.Application -import android.content.Context -import android.os.UserHandle - -class TestApplication : Application() { - -    // return the current context as a work profile doesn't really exist in these tests -    override fun createContextAsUser(user: UserHandle, flags: Int): Context = this -}
\ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt deleted file mode 100644 index d239f612..00000000 --- a/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt +++ /dev/null @@ -1,56 +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 - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import com.android.intentresolver.contentpreview.BasePreviewViewModel -import com.android.intentresolver.contentpreview.ImageLoader -import com.android.intentresolver.contentpreview.PreviewDataProvider - -/** A test content preview model that supports image loader override. */ -class TestContentPreviewViewModel( -    private val viewModel: BasePreviewViewModel, -    private val imageLoader: ImageLoader? = null, -) : BasePreviewViewModel() { -    override fun createOrReuseProvider( -        chooserRequest: ChooserRequestParameters -    ): PreviewDataProvider = viewModel.createOrReuseProvider(chooserRequest) - -    override fun createOrReuseImageLoader(): ImageLoader = -        imageLoader ?: viewModel.createOrReuseImageLoader() - -    companion object { -        fun wrap( -            factory: ViewModelProvider.Factory, -            imageLoader: ImageLoader?, -        ): ViewModelProvider.Factory = -            object : ViewModelProvider.Factory { -                @Suppress("UNCHECKED_CAST") -                override fun <T : ViewModel> create( -                    modelClass: Class<T>, -                    extras: CreationExtras -                ): T { -                    return TestContentPreviewViewModel( -                        factory.create(modelClass, extras) as BasePreviewViewModel, -                        imageLoader, -                    ) as T -                } -            } -    } -} diff --git a/java/tests/src/com/android/intentresolver/TestContentProvider.kt b/java/tests/src/com/android/intentresolver/TestContentProvider.kt deleted file mode 100644 index 426f9af2..00000000 --- a/java/tests/src/com/android/intentresolver/TestContentProvider.kt +++ /dev/null @@ -1,69 +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 - -import android.content.ContentProvider -import android.content.ContentValues -import android.database.Cursor -import android.net.Uri - -class TestContentProvider : ContentProvider() { -    override fun query( -        uri: Uri, -        projection: Array<out String>?, -        selection: String?, -        selectionArgs: Array<out String>?, -        sortOrder: String? -    ): Cursor? = null - -    override fun getType(uri: Uri): String? = -        runCatching { uri.getQueryParameter(PARAM_MIME_TYPE) }.getOrNull() - -    override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? { -        val delay = -            runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE_TIMEOUT)?.toLong() ?: 0L } -                .getOrDefault(0L) -        if (delay > 0) { -            try { -                Thread.sleep(delay) -            } catch (e: InterruptedException) { -                Thread.currentThread().interrupt() -            } -        } -        return runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE)?.let { arrayOf(it) } } -            .getOrNull() -    } - -    override fun insert(uri: Uri, values: ContentValues?): Uri? = null - -    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0 - -    override fun update( -        uri: Uri, -        values: ContentValues?, -        selection: String?, -        selectionArgs: Array<out String>? -    ): Int = 0 - -    override fun onCreate(): Boolean = true - -    companion object { -        const val PARAM_MIME_TYPE = "mimeType" -        const val PARAM_STREAM_TYPE = "streamType" -        const val PARAM_STREAM_TYPE_TIMEOUT = "streamTypeTo" -    } -} diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt deleted file mode 100644 index b9047712..00000000 --- a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt +++ /dev/null @@ -1,31 +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 - -import com.android.intentresolver.flags.FeatureFlagRepository -import com.android.systemui.flags.BooleanFlag -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag - -class TestFeatureFlagRepository( -    private val overrides: Map<BooleanFlag, Boolean> -) : FeatureFlagRepository { -    override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag) -    override fun isEnabled(flag: ReleasedFlag): Boolean = getValue(flag) - -    private fun getValue(flag: BooleanFlag) = overrides.getOrDefault(flag, flag.default) -} diff --git a/java/tests/src/com/android/intentresolver/TestHelpers.kt b/java/tests/src/com/android/intentresolver/TestHelpers.kt deleted file mode 100644 index 5b583fef..00000000 --- a/java/tests/src/com/android/intentresolver/TestHelpers.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 - -import android.app.prediction.AppTarget -import android.app.prediction.AppTargetId -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager.ShareShortcutInfo -import android.os.Bundle -import android.service.chooser.ChooserTarget -import org.mockito.Mockito.`when` as whenever - -internal fun createShareShortcutInfo( -    id: String, -    componentName: ComponentName, -    rank: Int -): ShareShortcutInfo = -    ShareShortcutInfo( -        createShortcutInfo(id, componentName, rank), -        componentName -    ) - -internal fun createShortcutInfo( -    id: String, -    componentName: ComponentName, -    rank: Int -): ShortcutInfo { -    val context = mock<Context>() -    whenever(context.packageName).thenReturn(componentName.packageName) -    return ShortcutInfo.Builder(context, id) -        .setShortLabel("Short Label $id") -        .setLongLabel("Long Label $id") -        .setActivity(componentName) -        .setRank(rank) -        .build() -} - -internal fun createAppTarget(shortcutInfo: ShortcutInfo) = -    AppTarget( -        AppTargetId(shortcutInfo.id), -        shortcutInfo, -        shortcutInfo.activity?.className ?: error("missing activity info") -    ) - -fun createChooserTarget( -    title: String, score: Float, componentName: ComponentName, shortcutId: String -): ChooserTarget = -    ChooserTarget( -        title, -        null, -        score, -        componentName, -        Bundle().apply { putString(Intent.EXTRA_SHORTCUT_ID, shortcutId) } -    ) diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt deleted file mode 100644 index bf87ed8a..00000000 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 - -import android.graphics.Bitmap -import android.net.Uri -import androidx.lifecycle.Lifecycle -import com.android.intentresolver.contentpreview.ImageLoader -import java.util.function.Consumer - -internal class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader { -    override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) { -        callback.accept(bitmaps[uri]) -    } - -    override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] - -    override fun prePopulate(uris: List<Uri>) = Unit -} diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java deleted file mode 100644 index b8b57403..00000000 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ /dev/null @@ -1,3112 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import static android.app.Activity.RESULT_OK; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.longClick; -import static androidx.test.espresso.action.ViewActions.swipeUp; -import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - -import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET; -import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT; -import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; -import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; -import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; -import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; -import static com.android.intentresolver.MatcherUtils.first; - -import static com.google.common.truth.Truth.assertThat; - -import static junit.framework.Assert.assertNull; - -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.PendingIntent; -import android.app.usage.UsageStatsManager; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.ClipboardManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager.ShareShortcutInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.os.Bundle; -import android.os.UserHandle; -import android.provider.DeviceConfig; -import android.service.chooser.ChooserAction; -import android.service.chooser.ChooserTarget; -import android.util.HashedStringCache; -import android.util.Pair; -import android.util.SparseArray; -import android.view.View; -import android.view.WindowManager; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.test.espresso.contrib.RecyclerViewActions; -import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; -import androidx.test.espresso.matcher.ViewMatchers; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.rule.ActivityTestRule; - -import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.contentpreview.ImageLoader; -import com.android.intentresolver.logging.EventLog; -import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.flags.BooleanFlag; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.Matchers; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.RuleChain; -import org.junit.rules.TestRule; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * Instrumentation tests for the IntentResolver module's Sharesheet (ChooserActivity). - * TODO: remove methods that supported running these tests against arbitrary ChooserActivity - * subclasses. Those were left over from an earlier version where IntentResolver's ChooserActivity - * inherited from the framework version at com.android.internal.app.ChooserActivity, and this test - * file inherited from the framework's version as well. Once the migration to the IntentResolver - * package is complete, that aspect of the test design can revert to match the style of the - * framework tests prior to ag/16482932. - * TODO: this can simply be renamed to "ChooserActivityTest" if that's ever unambiguous (i.e., if - * there's no risk of confusion with the framework tests that currently share the same name). - */ -@RunWith(Parameterized.class) -public class UnbundledChooserActivityTest { - -    /* -------- -     * Subclasses should copy the following section verbatim (or alternatively could specify some -     * additional @Parameterized.Parameters, as long as the correct parameters are used to -     * initialize the ChooserActivityTest). The subclasses should also be @RunWith the -     * `Parameterized` runner. -     * -------- -     */ - -    private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry -            .getInstrumentation().getTargetContext().getUser(); -    private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm; -    private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM = -            pm -> { -                PackageManager mock = Mockito.spy(pm); -                when(mock.getAppPredictionServicePackageName()).thenReturn(null); -                return mock; -            }; - -    private static final List<BooleanFlag> ALL_FLAGS = -            Arrays.asList(); - -    private static final Map<BooleanFlag, Boolean> ALL_FLAGS_OFF = -            createAllFlagsOverride(false); -    private static final Map<BooleanFlag, Boolean> ALL_FLAGS_ON = -            createAllFlagsOverride(true); - -    @Parameterized.Parameters -    public static Collection packageManagers() { -        if (ALL_FLAGS.isEmpty()) { -            // No flags to toggle between, so just two configurations. -            return Arrays.asList(new Object[][] { -                    // Default PackageManager and all flags off -                    { DEFAULT_PM, ALL_FLAGS_OFF}, -                    // No App Prediction Service and all flags off -                    { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, -            }); -        } -        return Arrays.asList(new Object[][] { -                // Default PackageManager and all flags off -                { DEFAULT_PM, ALL_FLAGS_OFF}, -                // Default PackageManager and all flags on -                { DEFAULT_PM, ALL_FLAGS_ON}, -                // No App Prediction Service and all flags off -                { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, -                // No App Prediction Service and all flags on -                { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_ON } -        }); -    } - -    private static Map<BooleanFlag, Boolean> createAllFlagsOverride(boolean value) { -        HashMap<BooleanFlag, Boolean> overrides = new HashMap<>(ALL_FLAGS.size()); -        for (BooleanFlag flag : ALL_FLAGS) { -            overrides.put(flag, value); -        } -        return overrides; -    } - -    /* -------- -     * Subclasses can override the following methods to customize test behavior. -     * -------- -     */ - -    /** -     * Perform any necessary per-test initialization steps (subclasses may add additional steps -     * before and/or after calling up to the superclass implementation). -     */ -    @CallSuper -    protected void setup() { -        // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the -        // permissions we require (which we'll read from the manifest at runtime). -        InstrumentationRegistry -                .getInstrumentation() -                .getUiAutomation() -                .adoptShellPermissionIdentity(); - -        cleanOverrideData(); -        ChooserActivityOverrideData.getInstance().featureFlagRepository = -                new TestFeatureFlagRepository(mFlags); -    } - -    /** -     * Given an intent that was constructed in a test, perform any additional configuration to -     * specify the appropriate concrete ChooserActivity subclass. The activity launched by this -     * intent must descend from android.intentresolver.ChooserActivity (for our ActivityTestRule), and -     * must also implement the android.intentresolver.IChooserWrapper interface (since test code will -     * assume the ability to make unsafe downcasts). -     */ -    protected Intent getConcreteIntentForLaunch(Intent clientIntent) { -        clientIntent.setClass( -                InstrumentationRegistry.getInstrumentation().getTargetContext(), -                com.android.intentresolver.ChooserWrapperActivity.class); -        return clientIntent; -    } - -    /** -     * Whether {@code #testIsAppPredictionServiceAvailable} should verify the behavior after -     * changing the availability conditions at runtime. In the unbundled chooser, the availability -     * is cached at start and will never be re-evaluated. -     * TODO: remove when we no longer want to test the system's on-the-fly evaluation. -     */ -    protected boolean shouldTestTogglingAppPredictionServiceAvailabilityAtRuntime() { -        return false; -    } - -    /* -------- -     * The code in this section is unorthodox and can be simplified/reverted when we no longer need -     * to support the parallel chooser implementations. -     * -------- -     */ - -    @Rule -    public final TestRule mRule; - -    // Shared test code references the activity under test as ChooserActivity, the common ancestor -    // of any (inheritance-based) chooser implementation. For testing purposes, that activity will -    // usually be cast to IChooserWrapper to expose instrumentation. -    private ActivityTestRule<ChooserActivity> mActivityRule = -            new ActivityTestRule<>(ChooserActivity.class, false, false) { -                @Override -                public ChooserActivity launchActivity(Intent clientIntent) { -                    return super.launchActivity(getConcreteIntentForLaunch(clientIntent)); -                } -            }; - -    @Before -    public final void doPolymorphicSetup() { -        // The base class needs a @Before-annotated setup for when it runs against the system -        // chooser, while subclasses need to be able to specify their own setup behavior. Notably -        // the unbundled chooser, running in user-space, needs to take additional steps before it -        // can run #cleanOverrideData() (which writes to DeviceConfig). -        setup(); -    } - -    /* -------- -     * Subclasses can ignore the remaining code and inherit the full suite of tests. -     * -------- -     */ - -    private static final String TEST_MIME_TYPE = "application/TestType"; - -    private static final int CONTENT_PREVIEW_IMAGE = 1; -    private static final int CONTENT_PREVIEW_FILE = 2; -    private static final int CONTENT_PREVIEW_TEXT = 3; - -    private final Function<PackageManager, PackageManager> mPackageManagerOverride; -    private final Map<BooleanFlag, Boolean> mFlags; - - -    public UnbundledChooserActivityTest( -                Function<PackageManager, PackageManager> packageManagerOverride, -                Map<BooleanFlag, Boolean> flags) { -        mPackageManagerOverride = packageManagerOverride; -        mFlags = flags; - -        mRule = RuleChain -                .outerRule(new FeatureFlagRule(flags)) -                .around(mActivityRule); -    } - -    private void setDeviceConfigProperty( -            @NonNull String propertyName, -            @NonNull String value) { -        // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly -        // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently -        // configure in {@link #setup()}. -        // TODO: is it really appropriate that this is always set with makeDefault=true? -        boolean valueWasSet = DeviceConfig.setProperty( -                DeviceConfig.NAMESPACE_SYSTEMUI, -                propertyName, -                value, -                true /* makeDefault */); -        if (!valueWasSet) { -            throw new IllegalStateException( -                        "Could not set " + propertyName + " to " + value); -        } -    } - -    public void cleanOverrideData() { -        ChooserActivityOverrideData.getInstance().reset(); -        ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride; - -        setDeviceConfigProperty( -                SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, -                Boolean.toString(true)); -    } - -    @Test -    public void customTitle() throws InterruptedException { -        Intent viewIntent = createViewTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity( -                Intent.createChooser(viewIntent, "chooser test")); - -        waitForIdle(); -        assertThat(activity.getAdapter().getCount(), is(2)); -        assertThat(activity.getAdapter().getServiceTargetCount(), is(0)); -        onView(withId(android.R.id.title)).check(matches(withText("chooser test"))); -    } - -    @Test -    public void customTitleIgnoredForSendIntents() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); -        waitForIdle(); -        onView(withId(android.R.id.title)) -                .check(matches(withText(R.string.whichSendApplication))); -    } - -    @Test -    public void emptyTitle() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(android.R.id.title)) -                .check(matches(withText(R.string.whichSendApplication))); -    } - -    @Test -    public void emptyPreviewTitleAndThumbnail() throws InterruptedException { -        Intent sendIntent = createSendTextIntentWithPreview(null, null); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.content_preview_title)) -                .check(matches(not(isDisplayed()))); -        onView(withId(com.android.internal.R.id.content_preview_thumbnail)) -                .check(matches(not(isDisplayed()))); -    } - -    @Test -    public void visiblePreviewTitleWithoutThumbnail() throws InterruptedException { -        String previewTitle = "My Content Preview Title"; -        Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.content_preview_title)) -                .check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.content_preview_title)) -                .check(matches(withText(previewTitle))); -        onView(withId(com.android.internal.R.id.content_preview_thumbnail)) -                .check(matches(not(isDisplayed()))); -    } - -    @Test -    public void visiblePreviewTitleWithInvalidThumbnail() throws InterruptedException { -        String previewTitle = "My Content Preview Title"; -        Intent sendIntent = createSendTextIntentWithPreview(previewTitle, -                Uri.parse("tel:(+49)12345789")); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.content_preview_title)) -                .check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.content_preview_thumbnail)) -                .check(matches(not(isDisplayed()))); -    } - -    @Test -    public void visiblePreviewTitleAndThumbnail() throws InterruptedException { -        String previewTitle = "My Content Preview Title"; -        Uri uri = Uri.parse( -                "android.resource://com.android.frameworks.coretests/" -                        + R.drawable.test320x240); -        Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.content_preview_title)) -                .check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.content_preview_thumbnail)) -                .check(matches(isDisplayed())); -    } - -    @Test @Ignore -    public void twoOptionsAndUserSelectsOne() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        assertThat(activity.getAdapter().getCount(), is(2)); -        onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); -        onView(withText(toChoose.activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test @Ignore -    public void fourOptionsStackedIntoOneTarget() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); - -        // create just enough targets to ensure the a-z list should be shown -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); - -        // next create 4 targets in a single app that should be stacked into a single target -        String packageName = "xxx.yyy"; -        String appName = "aaa"; -        ComponentName cn = new ComponentName(packageName, appName); -        Intent intent = new Intent("fakeIntent"); -        List<ResolvedComponentInfo> infosToStack = new ArrayList<>(); -        for (int i = 0; i < 4; i++) { -            ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i, -                    UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); -            resolveInfo.activityInfo.applicationInfo.name = appName; -            resolveInfo.activityInfo.applicationInfo.packageName = packageName; -            resolveInfo.activityInfo.packageName = packageName; -            resolveInfo.activityInfo.name = "ccc" + i; -            infosToStack.add(new ResolvedComponentInfo(cn, intent, resolveInfo)); -        } -        resolvedComponentInfos.addAll(infosToStack); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // expect 1 unique targets + 1 group + 4 ranked app targets -        assertThat(activity.getAdapter().getCount(), is(6)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        onView(allOf(withText(appName), hasSibling(withText("")))).perform(click()); -        waitForIdle(); - -        // clicking will launch a dialog to choose the activity within the app -        onView(withText(appName)).check(matches(isDisplayed())); -        int i = 0; -        for (ResolvedComponentInfo rci: infosToStack) { -            onView(withText("ccc" + i)).check(matches(isDisplayed())); -            ++i; -        } -    } - -    @Test @Ignore -    public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        UsageStatsManager usm = activity.getUsageStatsManager(); -        verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) -                .topK(any(List.class), anyInt()); -        assertThat(activity.getIsSelected(), is(false)); -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            return true; -        }; -        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); -        DisplayResolveInfo testDri = -                activity.createTestDisplayResolveInfo(sendIntent, toChoose, "testLabel", "testInfo", -                        sendIntent, /* resolveInfoPresentationGetter */ null); -        onView(withText(toChoose.activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) -                .updateChooserCounts(Mockito.anyString(), any(UserHandle.class), -                        Mockito.anyString()); -        verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) -                .updateModel(testDri); -        assertThat(activity.getIsSelected(), is(true)); -    } - -    @Ignore // b/148158199 -    @Test -    public void noResultsFromPackageManager() { -        setupResolverControllers(null); -        Intent sendIntent = createSendTextIntent(); -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        final IChooserWrapper wrapper = (IChooserWrapper) activity; - -        waitForIdle(); -        assertThat(activity.isFinishing(), is(false)); - -        onView(withId(android.R.id.empty)).check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed()))); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> wrapper.getAdapter().handlePackagesChanged() -        ); -        // backward compatibility. looks like we finish when data is empty after package change -        assertThat(activity.isFinishing(), is(true)); -    } - -    @Test -    public void autoLaunchSingleResult() throws InterruptedException { -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); -        setupResolverControllers(resolvedComponentInfos); - -        Intent sendIntent = createSendTextIntent(); -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); -        assertThat(activity.isFinishing(), is(true)); -    } - -    @Test @Ignore -    public void hasOtherProfileOneOption() { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        markWorkProfileUserAvailable(); - -        ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); -        Intent sendIntent = createSendTextIntent(); -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // The other entry is filtered to the other profile slot -        assertThat(activity.getAdapter().getCount(), is(1)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); -        waitForIdle(); - -        onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test @Ignore -    public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3); -        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - -        setupResolverControllers(resolvedComponentInfos); -        when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen()) -                .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // The other entry is filtered to the other profile slot -        assertThat(activity.getAdapter().getCount(), is(2)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(3); -        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test @Ignore -    public void hasLastChosenActivityAndOtherProfile() throws Exception { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3); -        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // The other entry is filtered to the last used slot -        assertThat(activity.getAdapter().getCount(), is(2)); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        // Make a stable copy of the components as the original list may be modified -        List<ResolvedComponentInfo> stableCopy = -                createResolvedComponentsForTestWithOtherProfile(3); -        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(toChoose)); -    } - -    @Test -    @Ignore("b/285309527") -    public void testFilePlusTextSharing_ExcludeText() { -        Uri uri = createTestContentProviderUri(null, "image/png"); -        Intent sendIntent = createSendImageIntent(uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); - -        List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.imageviewer", "ImageTarget"), -                        sendIntent, PERSONAL_USER_HANDLE), -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.textviewer", "UriTarget"), -                        new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) -        ); - -        setupResolverControllers(resolvedComponentInfos); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.include_text_action)) -                .check(matches(isDisplayed())) -                .perform(click()); -        waitForIdle(); - -        onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); - -        AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            launchedIntentRef.set(targetInfo.getTargetIntent()); -            return true; -        }; - -        onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(launchedIntentRef.get().hasExtra(Intent.EXTRA_TEXT)).isFalse(); -    } - -    @Test -    @Ignore("b/285309527") -    public void testFilePlusTextSharing_RemoveAndAddBackText() { -        Uri uri = createTestContentProviderUri("application/pdf", "image/png"); -        Intent sendIntent = createSendImageIntent(uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); -        final String text = "https://google.com/search?q=google"; -        sendIntent.putExtra(Intent.EXTRA_TEXT, text); - -        List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.imageviewer", "ImageTarget"), -                        sendIntent, PERSONAL_USER_HANDLE), -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.textviewer", "UriTarget"), -                        new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) -        ); - -        setupResolverControllers(resolvedComponentInfos); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.include_text_action)) -                .check(matches(isDisplayed())) -                .perform(click()); -        waitForIdle(); -        onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); - -        onView(withId(R.id.include_text_action)) -                .perform(click()); -        waitForIdle(); - -        onView(withId(R.id.content_preview_text)).check(matches(withText(text))); - -        AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            launchedIntentRef.set(targetInfo.getTargetIntent()); -            return true; -        }; - -        onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); -    } - -    @Test -    @Ignore("b/285309527") -    public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { -        Uri uri = createTestContentProviderUri("image/png", null); -        Intent sendIntent = createSendImageIntent(uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); - -        Intent alternativeIntent = createSendTextIntent(); -        final String text = "alternative intent"; -        alternativeIntent.putExtra(Intent.EXTRA_TEXT, text); - -        List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.imageviewer", "ImageTarget"), -                        sendIntent, PERSONAL_USER_HANDLE), -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.textviewer", "UriTarget"), -                        alternativeIntent, PERSONAL_USER_HANDLE) -        ); - -        setupResolverControllers(resolvedComponentInfos); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.include_text_action)) -                .check(matches(isDisplayed())) -                .perform(click()); -        waitForIdle(); - -        AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            launchedIntentRef.set(targetInfo.getTargetIntent()); -            return true; -        }; - -        onView(withText(resolvedComponentInfos.get(1).getResolveInfoAt(0).activityInfo.name)) -                .perform(click()); -        waitForIdle(); -        assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); -    } - -    @Test -    @Ignore("b/285309527") -    public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() { -        Uri uri = createTestContentProviderUri("image/png", null); -        Intent sendIntent = createSendImageIntent(uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                new TestPreviewImageLoader(Collections.emptyMap()); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); - -        List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.imageviewer", "ImageTarget"), -                        sendIntent, PERSONAL_USER_HANDLE), -                ResolverDataProvider.createResolvedComponentInfo( -                        new ComponentName("org.textviewer", "UriTarget"), -                        new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) -        ); - -        setupResolverControllers(resolvedComponentInfos); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.include_text_action)) -                .check(matches(isDisplayed())) -                .perform(click()); -        waitForIdle(); - -        onView(withId(R.id.image_view)) -                .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); -        onView(withId(R.id.content_preview_text)) -                .check(matches(allOf(isDisplayed(), withText("Image only")))); -    } - -    @Test -    public void copyTextToClipboard() { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.copy)).check(matches(isDisplayed())); -        onView(withId(R.id.copy)).perform(click()); -        ClipboardManager clipboard = (ClipboardManager) activity.getSystemService( -                Context.CLIPBOARD_SERVICE); -        ClipData clipData = clipboard.getPrimaryClip(); -        assertThat(clipData).isNotNull(); -        assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending"); - -        ClipDescription clipDescription = clipData.getDescription(); -        assertThat("text/plain", is(clipDescription.getMimeType(0))); - -        assertEquals(mActivityRule.getActivityResult().getResultCode(), RESULT_OK); -    } - -    @Test -    public void copyTextToClipboardLogging() { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(R.id.copy)).check(matches(isDisplayed())); -        onView(withId(R.id.copy)).perform(click()); - -        EventLog logger = activity.getEventLog(); -        verify(logger, times(1)).logActionSelected(eq(EventLog.SELECTION_TYPE_COPY)); -    } - -    @Test -    @Ignore -    public void testNearbyShareLogging() throws Exception { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(com.android.internal.R.id.chooser_nearby_button)) -                .check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click()); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - - - -    @Test @Ignore -    public void testEditImageLogs() { -        Uri uri = createTestContentProviderUri("image/png", null); -        Intent sendIntent = createSendImageIntent(uri); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click()); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - - -    @Test -    public void oneVisibleImagePreview() { -        Uri uri = createTestContentProviderUri("image/png", null); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createWideBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.scrollable_image_preview)) -                .check((view, exception) -> { -                    if (exception != null) { -                        throw exception; -                    } -                    RecyclerView recyclerView = (RecyclerView) view; -                    assertThat(recyclerView.getAdapter().getItemCount(), is(1)); -                    assertThat(recyclerView.getChildCount(), is(1)); -                    View imageView = recyclerView.getChildAt(0); -                    Rect rect = new Rect(); -                    boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect); -                    assertThat( -                            "image preview view is not fully visible", -                            isPartiallyVisible -                                    && rect.width() == imageView.getWidth() -                                    && rect.height() == imageView.getHeight()); -                }); -    } - -    @Test -    public void allThumbnailsFailedToLoad_hidePreview() { -        Uri uri = createTestContentProviderUri("image/jpg", null); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                new TestPreviewImageLoader(Collections.emptyMap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.scrollable_image_preview)) -                .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); -    } - -    @Test -    public void testSlowUriMetadata_fallbackToFilePreview() throws InterruptedException { -        Uri uri = createTestContentProviderUri( -                "application/pdf", "image/png", /*streamTypeTimeout=*/4_000); -        ArrayList<Uri> uris = new ArrayList<>(1); -        uris.add(uri); -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000)) -                .isTrue(); -        waitForIdle(); - -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - -    @Test -    public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi() -            throws InterruptedException { -        Uri fileUri = createTestContentProviderUri( -                "application/pdf", "application/pdf", /*streamTypeTimeout=*/150); -        Uri imageUri = createTestContentProviderUri("application/pdf", "image/png"); -        ArrayList<Uri> uris = new ArrayList<>(50); -        for (int i = 0; i < 49; i++) { -            uris.add(fileUri); -        } -        uris.add(imageUri); -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(imageUri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); -        assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000)) -                .isTrue(); - -        waitForIdle(); - -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - -    @Test -    public void testManyVisibleImagePreview_ScrollableImagePreview() { -        Uri uri = createTestContentProviderUri("image/png", null); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.scrollable_image_preview)) -                .perform(RecyclerViewActions.scrollToLastPosition()) -                .check((view, exception) -> { -                    if (exception != null) { -                        throw exception; -                    } -                    RecyclerView recyclerView = (RecyclerView) view; -                    assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size())); -                }); -    } - -    @Test -    public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart() -            throws InterruptedException { -        Uri imgOneUri = createTestContentProviderUri("image/png", null); -        Uri imgTwoUri = createTestContentProviderUri("image/png", null) -                .buildUpon() -                .path("image-2.png") -                .build(); -        Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 3_000); -        ArrayList<Uri> uris = new ArrayList<>(2); -        // two large previews to fill the screen and be presented right away and one -        // document that would be delayed by the URI metadata reading -        uris.add(imgOneUri); -        uris.add(imgTwoUri); -        uris.add(docUri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        Map<Uri, Bitmap> bitmaps = new HashMap<>(); -        bitmaps.put(imgOneUri, createWideBitmap(Color.RED)); -        bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN)); -        bitmaps.put(docUri, createWideBitmap(Color.BLUE)); -        ChooserActivityOverrideData.getInstance().imageLoader = -                new TestPreviewImageLoader(bitmaps); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 1_000)) -                .isTrue(); -        waitForIdle(); - -        onView(withId(R.id.scrollable_image_preview)) -                .check((view, exception) -> { -                    if (exception != null) { -                        throw exception; -                    } -                    RecyclerView recyclerView = (RecyclerView) view; -                    assertThat(recyclerView.getChildCount()).isAtLeast(1); -                    // the first view is a preview -                    View imageView = recyclerView.getChildAt(0).findViewById(R.id.image); -                    assertThat(imageView).isNotNull(); -                }) -                .perform(RecyclerViewActions.scrollToLastPosition()) -                .check((view, exception) -> { -                    if (exception != null) { -                        throw exception; -                    } -                    RecyclerView recyclerView = (RecyclerView) view; -                    assertThat(recyclerView.getChildCount()).isAtLeast(1); -                    // check that the last view is a loading indicator -                    View loadingIndicator = -                            recyclerView.getChildAt(recyclerView.getChildCount() - 1); -                    assertThat(loadingIndicator).isNotNull(); -                }); -        waitForIdle(); -    } - -    @Test -    public void testImageAndTextPreview() { -        final Uri uri = createTestContentProviderUri("image/png", null); -        final String sharedText = "text-" + System.currentTimeMillis(); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withText(sharedText)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testTextPreviewWhenTextIsSharedWithMultipleImages() { -        final Uri uri = createTestContentProviderUri("image/png", null); -        final String sharedText = "text-" + System.currentTimeMillis(); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .resolverListController -                        .getResolversForIntentAsUser( -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.isA(List.class), -                                Mockito.any(UserHandle.class))) -                .thenReturn(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withText(sharedText)).check(matches(isDisplayed())); -    } - -    @Test -    public void testOnCreateLogging() { -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); -        EventLog logger = activity.getEventLog(); -        waitForIdle(); - -        verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong()); -    } - -    @Test -    public void testOnCreateLoggingFromWorkProfile() { -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        ChooserActivityOverrideData.getInstance().alternateProfileSetting = -                MetricsEvent.MANAGED_PROFILE; - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); -        EventLog logger = activity.getEventLog(); -        waitForIdle(); - -        verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong()); -    } - -    @Test -    public void testEmptyPreviewLogging() { -        Intent sendIntent = createSendTextIntentWithPreview(null, null); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity( -                        Intent.createChooser(sendIntent, "empty preview logger test")); -        EventLog logger = activity.getEventLog(); -        waitForIdle(); - -        verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong()); -    } - -    @Test -    public void testTitlePreviewLogging() { -        Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // Second invocation is from onCreate -        EventLog logger = activity.getEventLog(); -        Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT)); -    } - -    @Test -    public void testImagePreviewLogging() { -        Uri uri = createTestContentProviderUri("image/png", null); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); -        ChooserActivityOverrideData.getInstance().imageLoader = -                createImageLoader(uri, createBitmap()); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        EventLog logger = activity.getEventLog(); -        Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE)); -    } - -    @Test -    public void oneVisibleFilePreview() throws InterruptedException { -        Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - - -    @Test -    public void moreThanOneVisibleFilePreview() throws InterruptedException { -        Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); -        uris.add(uri); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); -        onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 2 more files"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - -    @Test -    public void contentProviderThrowSecurityException() throws InterruptedException { -        Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        ChooserActivityOverrideData.getInstance().resolverForceException = true; - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - -    @Test -    public void contentProviderReturnsNoColumns() throws InterruptedException { -        Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); - -        ArrayList<Uri> uris = new ArrayList<>(); -        uris.add(uri); -        uris.add(uri); - -        Intent sendIntent = createSendUriIntentWithPreview(uris); - -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        Cursor cursor = mock(Cursor.class); -        when(cursor.getCount()).thenReturn(1); -        Mockito.doNothing().when(cursor).close(); -        when(cursor.moveToFirst()).thenReturn(true); -        when(cursor.getColumnIndex(Mockito.anyString())).thenReturn(-1); - -        ChooserActivityOverrideData.getInstance().resolverCursor = cursor; - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); -        onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); -        onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); -        onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 1 more file"))); -        onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); -    } - -    @Test -    public void testGetBaseScore() { -        final float testBaseScore = 0.89f; - -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .resolverListController -                        .getScore(Mockito.isA(DisplayResolveInfo.class))) -                .thenReturn(testBaseScore); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        final DisplayResolveInfo testDri = -                activity.createTestDisplayResolveInfo( -                        sendIntent, -                        ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), -                        "testLabel", -                        "testInfo", -                        sendIntent, -                /* resolveInfoPresentationGetter */ null); -        final ChooserListAdapter adapter = activity.getAdapter(); - -        assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST)); -        assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore)); -        assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_CHOOSER_TARGET), is(testBaseScore)); -        assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE), -                is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); -        assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER), -                is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); -    } - -    // This test is too long and too slow and should not be taken as an example for future tests. -    @Test -    public void testDirectTargetSelectionLogging() { -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                createShortcutLoaderFactory(); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                true, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        final ChooserListAdapter activeAdapter = activity.getAdapter(); -        assertThat( -                "Chooser should have 3 targets (2 apps, 1 direct)", -                activeAdapter.getCount(), -                is(3)); -        assertThat( -                "Chooser should have exactly one selectable direct target", -                activeAdapter.getSelectableServiceTargetCount(), -                is(1)); -        assertThat( -                "The resolver info must match the resolver info used to create the target", -                activeAdapter.getItem(0).getResolveInfo(), -                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - -        // Click on the direct target -        String name = serviceTargets.get(0).getTitle().toString(); -        onView(withText(name)) -                .perform(click()); -        waitForIdle(); - -        ArgumentCaptor<HashedStringCache.HashResult> hashCaptor = -                ArgumentCaptor.forClass(HashedStringCache.HashResult.class); -        verify(activity.getEventLog(), times(1)).logShareTargetSelected( -                eq(EventLog.SELECTION_TYPE_SERVICE), -                /* packageName= */ any(), -                /* positionPicked= */ anyInt(), -                /* directTargetAlsoRanked= */ eq(-1), -                /* numCallerProvided= */ anyInt(), -                /* directTargetHashed= */ hashCaptor.capture(), -                /* isPinned= */ anyBoolean(), -                /* successfullySelected= */ anyBoolean(), -                /* selectionCost= */ anyLong()); -        String hashedName = hashCaptor.getValue().hashedString; -        assertThat( -                "Hash is not predictable but must be obfuscated", -                hashedName, is(not(name))); -    } - -    // This test is too long and too slow and should not be taken as an example for future tests. -    @Test -    public void testDirectTargetLoggingWithRankedAppTarget() { -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                createShortcutLoaderFactory(); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        List<ChooserTarget> serviceTargets = createDirectShareTargets( -                1, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                true, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        final ChooserListAdapter activeAdapter = activity.getAdapter(); -        assertThat( -                "Chooser should have 3 targets (2 apps, 1 direct)", -                activeAdapter.getCount(), -                is(3)); -        assertThat( -                "Chooser should have exactly one selectable direct target", -                activeAdapter.getSelectableServiceTargetCount(), -                is(1)); -        assertThat( -                "The resolver info must match the resolver info used to create the target", -                activeAdapter.getItem(0).getResolveInfo(), -                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - -        // Click on the direct target -        String name = serviceTargets.get(0).getTitle().toString(); -        onView(withText(name)) -                .perform(click()); -        waitForIdle(); - -        verify(activity.getEventLog(), times(1)).logShareTargetSelected( -                eq(EventLog.SELECTION_TYPE_SERVICE), -                /* packageName= */ any(), -                /* positionPicked= */ anyInt(), -                /* directTargetAlsoRanked= */ eq(0), -                /* numCallerProvided= */ anyInt(), -                /* directTargetHashed= */ any(), -                /* isPinned= */ anyBoolean(), -                /* successfullySelected= */ anyBoolean(), -                /* selectionCost= */ anyLong()); -    } - -    @Test -    public void testShortcutTargetWithApplyAppLimits() { -        // Set up resources -        Resources resources = Mockito.spy( -                InstrumentationRegistry.getInstrumentation().getContext().getResources()); -        ChooserActivityOverrideData.getInstance().resources = resources; -        doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                createShortcutLoaderFactory(); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) mActivityRule -                .launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        List<ChooserTarget> serviceTargets = createDirectShareTargets( -                2, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                true, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        final ChooserListAdapter activeAdapter = activity.getAdapter(); -        assertThat( -                "Chooser should have 3 targets (2 apps, 1 direct)", -                activeAdapter.getCount(), -                is(3)); -        assertThat( -                "Chooser should have exactly one selectable direct target", -                activeAdapter.getSelectableServiceTargetCount(), -                is(1)); -        assertThat( -                "The resolver info must match the resolver info used to create the target", -                activeAdapter.getItem(0).getResolveInfo(), -                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); -        assertThat( -                "The display label must match", -                activeAdapter.getItem(0).getDisplayLabel(), -                is("testTitle0")); -    } - -    @Test -    public void testShortcutTargetWithoutApplyAppLimits() { -        setDeviceConfigProperty( -                SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, -                Boolean.toString(false)); -        // Set up resources -        Resources resources = Mockito.spy( -                InstrumentationRegistry.getInstrumentation().getContext().getResources()); -        ChooserActivityOverrideData.getInstance().resources = resources; -        doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                createShortcutLoaderFactory(); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        List<ChooserTarget> serviceTargets = createDirectShareTargets( -                2, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                true, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        final ChooserListAdapter activeAdapter = activity.getAdapter(); -        assertThat( -                "Chooser should have 4 targets (2 apps, 2 direct)", -                activeAdapter.getCount(), -                is(4)); -        assertThat( -                "Chooser should have exactly two selectable direct target", -                activeAdapter.getSelectableServiceTargetCount(), -                is(2)); -        assertThat( -                "The resolver info must match the resolver info used to create the target", -                activeAdapter.getItem(0).getResolveInfo(), -                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); -        assertThat( -                "The display label must match", -                activeAdapter.getItem(0).getDisplayLabel(), -                is("testTitle0")); -        assertThat( -                "The display label must match", -                activeAdapter.getItem(1).getDisplayLabel(), -                is("testTitle1")); -    } - -    @Test -    public void testLaunchWithCallerProvidedTarget() { -        setDeviceConfigProperty( -                SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, -                Boolean.toString(false)); -        // Set up resources -        Resources resources = Mockito.spy( -                InstrumentationRegistry.getInstrumentation().getContext().getResources()); -        ChooserActivityOverrideData.getInstance().resources = resources; -        doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); - -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos); -        markWorkProfileUserAvailable(); - -        // set caller-provided target -        Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); -        String callerTargetLabel = "Caller Target"; -        ChooserTarget[] targets = new ChooserTarget[] { -                new ChooserTarget( -                        callerTargetLabel, -                        Icon.createWithBitmap(createBitmap()), -                        0.1f, -                        resolvedComponentInfos.get(0).name, -                        new Bundle()) -        }; -        chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                createShortcutLoaderFactory(); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                true, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[0], -                new HashMap<>(), -                new HashMap<>()); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        final ChooserListAdapter activeAdapter = activity.getAdapter(); -        assertThat( -                "Chooser should have 3 targets (2 apps, 1 direct)", -                activeAdapter.getCount(), -                is(3)); -        assertThat( -                "Chooser should have exactly two selectable direct target", -                activeAdapter.getSelectableServiceTargetCount(), -                is(1)); -        assertThat( -                "The display label must match", -                activeAdapter.getItem(0).getDisplayLabel(), -                is(callerTargetLabel)); - -        // Switch to work profile and ensure that the target *doesn't* show up there. -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        for (int i = 0; i < activity.getWorkListAdapter().getCount(); i++) { -            assertThat( -                    "Chooser target should not show up in opposite profile", -                    activity.getWorkListAdapter().getItem(i).getDisplayLabel(), -                    not(callerTargetLabel)); -        } -    } - -    @Test -    public void testLaunchWithCustomAction() throws InterruptedException { -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); -        final String customActionLabel = "Custom Action"; -        final String testAction = "test-broadcast-receiver-action"; -        Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); -        chooserIntent.putExtra( -                Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, -                new ChooserAction[] { -                        new ChooserAction.Builder( -                                Icon.createWithResource("", Resources.ID_NULL), -                                customActionLabel, -                                PendingIntent.getBroadcast( -                                        testContext, -                                        123, -                                        new Intent(testAction), -                                        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)) -                                .build() -                }); -        // Start activity -        mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        final CountDownLatch broadcastInvoked = new CountDownLatch(1); -        BroadcastReceiver testReceiver = new BroadcastReceiver() { -            @Override -            public void onReceive(Context context, Intent intent) { -                broadcastInvoked.countDown(); -            } -        }; -        testContext.registerReceiver(testReceiver, new IntentFilter(testAction)); - -        try { -            onView(withText(customActionLabel)).perform(click()); -            broadcastInvoked.await(); -        } finally { -            testContext.unregisterReceiver(testReceiver); -        } -    } - -    @Test -    public void testLaunchWithShareModification() throws InterruptedException { -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); -        final String modifyShareAction = "test-broadcast-receiver-action"; -        Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); -        String label = "modify share"; -        PendingIntent pendingIntent = PendingIntent.getBroadcast( -                testContext, -                123, -                new Intent(modifyShareAction), -                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); -        ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap( -                createBitmap()), label, pendingIntent).build(); -        chooserIntent.putExtra( -                Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, -                action); -        // Start activity -        mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        final CountDownLatch broadcastInvoked = new CountDownLatch(1); -        BroadcastReceiver testReceiver = new BroadcastReceiver() { -            @Override -            public void onReceive(Context context, Intent intent) { -                broadcastInvoked.countDown(); -            } -        }; -        testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction)); - -        try { -            onView(withText(label)).perform(click()); -            broadcastInvoked.await(); -        } finally { -            testContext.unregisterReceiver(testReceiver); -        } -    } - -    @Test -    public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException { -        updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4); -        givenAppTargets(/* appCount= */ 16); -        Intent sendIntent = createSendTextIntent(); -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - -        updateMaxTargetsPerRowResource(/* targetsPerRow= */ 6); -        InstrumentationRegistry.getInstrumentation() -                .runOnMainSync(() -> activity.onConfigurationChanged( -                        InstrumentationRegistry.getInstrumentation() -                                .getContext().getResources().getConfiguration())); - -        waitForIdle(); -        onView(withId(com.android.internal.R.id.resolver_list)) -                .check(matches(withGridColumnCount(6))); -    } - -    // This test is too long and too slow and should not be taken as an example for future tests. -    @Test @Ignore -    public void testDirectTargetLoggingWithAppTargetNotRankedPortrait() -            throws InterruptedException { -        testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_PORTRAIT, 4); -    } - -    @Test @Ignore -    public void testDirectTargetLoggingWithAppTargetNotRankedLandscape() -            throws InterruptedException { -        testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_LANDSCAPE, 8); -    } - -    private void testDirectTargetLoggingWithAppTargetNotRanked( -            int orientation, int appTargetsExpected) { -        Configuration configuration = -                new Configuration(InstrumentationRegistry.getInstrumentation().getContext() -                        .getResources().getConfiguration()); -        configuration.orientation = orientation; - -        Resources resources = Mockito.spy( -                InstrumentationRegistry.getInstrumentation().getContext().getResources()); -        ChooserActivityOverrideData.getInstance().resources = resources; -        doReturn(configuration).when(resources).getConfiguration(); - -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15); -        setupResolverControllers(resolvedComponentInfos); - -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, -                resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE); - -        // Start activity -        final IChooserWrapper wrapper = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        // Insert the direct share target -        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); -        directShareToShortcutInfos.put(serviceTargets.get(0), null); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> wrapper.getAdapter().addServiceResults( -                        wrapper.createTestDisplayResolveInfo(sendIntent, -                                ri, -                                "testLabel", -                                "testInfo", -                                sendIntent, -                                /* resolveInfoPresentationGetter */ null), -                        serviceTargets, -                        TARGET_TYPE_CHOOSER_TARGET, -                        directShareToShortcutInfos, -                        /* directShareToAppTargets */ null) -        ); - -        assertThat( -                String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)", -                        appTargetsExpected + 16, appTargetsExpected), -                wrapper.getAdapter().getCount(), is(appTargetsExpected + 16)); -        assertThat("Chooser should have exactly one selectable direct target", -                wrapper.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat("The resolver info must match the resolver info used to create the target", -                wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); - -        // Click on the direct target -        String name = serviceTargets.get(0).getTitle().toString(); -        onView(withText(name)) -                .perform(click()); -        waitForIdle(); - -        EventLog logger = wrapper.getEventLog(); -        verify(logger, times(1)).logShareTargetSelected( -                eq(EventLog.SELECTION_TYPE_SERVICE), -                /* packageName= */ any(), -                /* positionPicked= */ anyInt(), -                // The packages sholdn't match for app target and direct target: -                /* directTargetAlsoRanked= */ eq(-1), -                /* numCallerProvided= */ anyInt(), -                /* directTargetHashed= */ any(), -                /* isPinned= */ anyBoolean(), -                /* successfullySelected= */ anyBoolean(), -                /* selectionCost= */ anyLong()); -    } - -    @Test -    public void testWorkTab_displayedWhenWorkProfileUserAvailable() { -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        markWorkProfileUserAvailable(); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); - -        onView(withId(android.R.id.tabs)).check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); - -        onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed()))); -    } - -    @Test -    public void testWorkTab_eachTabUsesExpectedAdapter() { -        int personalProfileTargets = 3; -        int otherProfileTargets = 1; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile( -                        personalProfileTargets + otherProfileTargets, /* userID */ 10); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( -                workProfileTargets); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        markWorkProfileUserAvailable(); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); - -        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); -        assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets)); -        assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); -    } - -    @Test -    public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); -    } - -    @Test @Ignore -    public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(first(allOf( -                withText(workResolvedComponentInfos.get(0) -                        .getResolveInfoAt(0).activityInfo.applicationInfo.name), -                isDisplayed()))) -                .perform(click()); -        waitForIdle(); -        assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); -    } - -    @Test -    public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); - -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_workProfileDisabled_emptyStateShown() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_turn_on_work_apps)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_no_work_apps_available)) -                .check(matches(isDisplayed())); -    } - -    @Ignore // b/220067877 -    @Test -    public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; -        ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_no_work_apps_available)) -                .check(matches(isDisplayed())); -    } - -    @Test @Ignore("b/222124533") -    public void testAppTargetLogging() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // TODO(b/222124533): other test cases use a timeout to make sure that the UI is fully -        // populated; without one, this test flakes. Ideally we should address the need for a -        // timeout everywhere instead of introducing one to fix this particular test. - -        assertThat(activity.getAdapter().getCount(), is(2)); -        onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); - -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); -        onView(withText(toChoose.activityInfo.name)) -                .perform(click()); -        waitForIdle(); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - -    @Test -    public void testDirectTargetLogging() { -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                new SparseArray<>(); -        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = -                (userHandle, callback) -> { -                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = -                            new Pair<>(mock(ShortcutLoader.class), callback); -                    shortcutLoaders.put(userHandle.getIdentifier(), pair); -                    return pair.first; -                }; - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)) -                .updateAppTargets(appTargets.capture()); - -        // send shortcuts -        assertThat( -                "Wrong number of app targets", -                appTargets.getValue().length, -                is(resolvedComponentInfos.size())); -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                // TODO: test another value as well -                false, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        assertThat("Chooser should have 3 targets (2 apps, 1 direct)", -                activity.getAdapter().getCount(), is(3)); -        assertThat("Chooser should have exactly one selectable direct target", -                activity.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat( -                "The resolver info must match the resolver info used to create the target", -                activity.getAdapter().getItem(0).getResolveInfo(), -                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - -        // Click on the direct target -        String name = serviceTargets.get(0).getTitle().toString(); -        onView(withText(name)) -                .perform(click()); -        waitForIdle(); - -        EventLog logger = activity.getEventLog(); -        ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class); -        verify(logger, times(1)).logShareTargetSelected( -                eq(EventLog.SELECTION_TYPE_SERVICE), -                /* packageName= */ any(), -                /* positionPicked= */ anyInt(), -                /* directTargetAlsoRanked= */ anyInt(), -                /* numCallerProvided= */ anyInt(), -                /* directTargetHashed= */ any(), -                /* isPinned= */ anyBoolean(), -                /* successfullySelected= */ anyBoolean(), -                /* selectionCost= */ anyLong()); -    } - -    @Test -    public void testDirectTargetPinningDialog() { -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // create test shortcut loader factory, remember loaders and their callbacks -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                new SparseArray<>(); -        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = -                (userHandle, callback) -> { -                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = -                            new Pair<>(mock(ShortcutLoader.class), callback); -                    shortcutLoaders.put(userHandle.getIdentifier(), pair); -                    return pair.first; -                }; - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        // verify that ShortcutLoader was queried -        ArgumentCaptor<DisplayResolveInfo[]> appTargets = -                ArgumentCaptor.forClass(DisplayResolveInfo[].class); -        verify(shortcutLoaders.get(0).first, times(1)) -                .updateAppTargets(appTargets.capture()); - -        // send shortcuts -        List<ChooserTarget> serviceTargets = createDirectShareTargets( -                1, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ShortcutLoader.Result result = new ShortcutLoader.Result( -                // TODO: test another value as well -                false, -                appTargets.getValue(), -                new ShortcutLoader.ShortcutResultInfo[] { -                        new ShortcutLoader.ShortcutResultInfo( -                                appTargets.getValue()[0], -                                serviceTargets -                        ) -                }, -                new HashMap<>(), -                new HashMap<>() -        ); -        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); -        waitForIdle(); - -        // Long-click on the direct target -        String name = serviceTargets.get(0).getTitle().toString(); -        onView(withText(name)).perform(longClick()); -        waitForIdle(); - -        onView(withId(R.id.chooser_dialog_content)).check(matches(isDisplayed())); -    } - -    @Test @Ignore -    public void testEmptyDirectRowLogging() throws InterruptedException { -        Intent sendIntent = createSendTextIntent(); -        // We need app targets for direct targets to get displayed -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); -        setupResolverControllers(resolvedComponentInfos); - -        // Start activity -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - -        // Thread.sleep shouldn't be a thing in an integration test but it's -        // necessary here because of the way the code is structured -        Thread.sleep(3000); - -        assertThat("Chooser should have 2 app targets", -                activity.getAdapter().getCount(), is(2)); -        assertThat("Chooser should have no direct targets", -                activity.getAdapter().getSelectableServiceTargetCount(), is(0)); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - -    @Ignore // b/220067877 -    @Test -    public void testCopyTextToClipboardLogging() throws Exception { -        Intent sendIntent = createSendTextIntent(); -        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - -        setupResolverControllers(resolvedComponentInfos); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        waitForIdle(); - -        onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); -        onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - -    @Test @Ignore("b/222124533") -    public void testSwitchProfileLogging() throws InterruptedException { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); -        onView(withText(R.string.resolver_personal_tab)).perform(click()); -        waitForIdle(); - -        // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. -    } - -    @Test -    public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); -        waitForIdle(); - -        assertNull(chosen[0]); -    } - -    @Test -    public void testOneInitialIntent_noAutolaunch() { -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(1); -        setupResolverControllers(personalResolvedComponentInfos); -        Intent chooserIntent = createChooserIntent(createSendTextIntent(), -                new Intent[] {new Intent("action.fake")}); -        ResolveInfo[] chosen = new ResolveInfo[1]; -        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { -            chosen[0] = targetInfo.getResolveInfo(); -            return true; -        }; -        ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); -        ResolveInfo ri = createFakeResolveInfo(); -        when( -                ChooserActivityOverrideData -                        .getInstance().packageManager -                        .resolveActivity(any(Intent.class), any())) -                .thenReturn(ri); -        waitForIdle(); - -        IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        assertNull(chosen[0]); -        assertThat(activity -                .getPersonalListAdapter().getCallerTargetCount(), is(1)); -    } - -    @Test -    public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 1; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent[] initialIntents = { -                new Intent("action.fake1"), -                new Intent("action.fake2") -        }; -        Intent chooserIntent = createChooserIntent(createSendTextIntent(), initialIntents); -        ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .packageManager -                        .resolveActivity(any(Intent.class), any())) -                .thenReturn(createFakeResolveInfo()); -        waitForIdle(); - -        IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        assertThat(activity.getPersonalListAdapter().getCallerTargetCount(), is(2)); -        assertThat(activity.getWorkListAdapter().getCallerTargetCount(), is(0)); -    } - -    @Test -    public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets); -        ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent[] initialIntents = { -                new Intent("action.fake1"), -                new Intent("action.fake2") -        }; -        Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); -        ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .packageManager -                        .resolveActivity(any(Intent.class), any())) -                .thenReturn(createFakeResolveInfo()); - -        mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); - -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(0); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent[] initialIntents = { -                new Intent("action.fake1"), -                new Intent("action.fake2") -        }; -        Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); -        ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .packageManager -                        .resolveActivity(any(Intent.class), any())) -                .thenReturn(createFakeResolveInfo()); - -        mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        onView(withText(R.string.resolver_no_work_apps_available)) -                .check(matches(isDisplayed())); -    } - -    @Test -    public void testDeduplicateCallerTargetRankedTarget() { -        // Create 4 ranked app targets. -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(4); -        setupResolverControllers(personalResolvedComponentInfos); -        // Create caller target which is duplicate with one of app targets -        Intent chooserIntent = createChooserIntent(createSendTextIntent(), -                new Intent[] {new Intent("action.fake")}); -        ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(0, -                UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .packageManager -                        .resolveActivity(any(Intent.class), any())) -                .thenReturn(ri); -        waitForIdle(); - -        IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); -        waitForIdle(); - -        // Total 4 targets (1 caller target, 3 ranked targets) -        assertThat(activity.getAdapter().getCount(), is(4)); -        assertThat(activity.getAdapter().getCallerTargetCount(), is(1)); -        assertThat(activity.getAdapter().getRankedTargetCount(), is(3)); -    } - -    @Test -    public void test_query_shortcut_loader_for_the_selected_tab() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); -        ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); -        final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>(); -        shortcutLoaders.put(0, personalProfileShortcutLoader); -        shortcutLoaders.put(10, workProfileShortcutLoader); -        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = -                (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        waitForIdle(); - -        verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any()); - -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        verify(workProfileShortcutLoader, times(1)).updateAppTargets(any()); -    } - -    @Test -    public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { -        // enable cloneProfile -        markCloneProfileUserAvailable(); -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsWithCloneProfileForTest( -                        3, -                        PERSONAL_USER_HANDLE, -                        ChooserActivityOverrideData.getInstance().cloneProfileUserHandle); -        setupResolverControllers(resolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); - -        final IChooserWrapper activity = (IChooserWrapper) mActivityRule -                .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest")); -        waitForIdle(); - -        assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE)); -        assertThat(activity.getAdapter().getCount(), is(3)); -    } - -    @Test -    public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { -        markWorkProfileUserAvailable(); -        markCloneProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( -                4); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - - -        final IChooserWrapper activity = (IChooserWrapper) -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test")); -        waitForIdle(); - -        assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); -    } - -    private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { -        Intent chooserIntent = new Intent(); -        chooserIntent.setAction(Intent.ACTION_CHOOSER); -        chooserIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); -        chooserIntent.putExtra(Intent.EXTRA_TITLE, "some title"); -        chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); -        chooserIntent.setType("text/plain"); -        if (initialIntents != null) { -            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents); -        } -        return chooserIntent; -    } - -    /* This is a "test of a test" to make sure that our inherited test class -     * is successfully configured to operate on the unbundled-equivalent -     * ChooserWrapperActivity. -     * -     * TODO: remove after unbundling is complete. -     */ -    @Test -    public void testWrapperActivityHasExpectedConcreteType() { -        final ChooserActivity activity = mActivityRule.launchActivity( -                Intent.createChooser(new Intent("ACTION_FOO"), "foo")); -        waitForIdle(); -        assertThat(activity).isInstanceOf(com.android.intentresolver.ChooserWrapperActivity.class); -    } - -    private ResolveInfo createFakeResolveInfo() { -        ResolveInfo ri = new ResolveInfo(); -        ri.activityInfo = new ActivityInfo(); -        ri.activityInfo.name = "FakeActivityName"; -        ri.activityInfo.packageName = "fake.package.name"; -        ri.activityInfo.applicationInfo = new ApplicationInfo(); -        ri.activityInfo.applicationInfo.packageName = "fake.package.name"; -        ri.userHandle = UserHandle.CURRENT; -        return ri; -    } - -    private Intent createSendTextIntent() { -        Intent sendIntent = new Intent(); -        sendIntent.setAction(Intent.ACTION_SEND); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); -        sendIntent.setType("text/plain"); -        return sendIntent; -    } - -    private Intent createSendImageIntent(Uri imageThumbnail) { -        Intent sendIntent = new Intent(); -        sendIntent.setAction(Intent.ACTION_SEND); -        sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail); -        sendIntent.setType("image/png"); -        if (imageThumbnail != null) { -            ClipData.Item clipItem = new ClipData.Item(imageThumbnail); -            sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); -        } - -        return sendIntent; -    } - -    private Uri createTestContentProviderUri( -            @Nullable String mimeType, @Nullable String streamType) { -        return createTestContentProviderUri(mimeType, streamType, 0); -    } - -    private Uri createTestContentProviderUri( -            @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) { -        String packageName = -                InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); -        Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png") -                .buildUpon(); -        if (mimeType != null) { -            builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType); -        } -        if (streamType != null) { -            builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType); -        } -        if (streamTypeTimeout > 0) { -            builder.appendQueryParameter( -                    TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT, -                    Long.toString(streamTypeTimeout)); -        } -        return builder.build(); -    } - -    private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) { -        Intent sendIntent = new Intent(); -        sendIntent.setAction(Intent.ACTION_SEND); -        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); -        sendIntent.putExtra(Intent.EXTRA_TITLE, title); -        if (imageThumbnail != null) { -            ClipData.Item clipItem = new ClipData.Item(imageThumbnail); -            sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); -        } - -        return sendIntent; -    } - -    private Intent createSendUriIntentWithPreview(ArrayList<Uri> uris) { -        Intent sendIntent = new Intent(); - -        if (uris.size() > 1) { -            sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE); -            sendIntent.putExtra(Intent.EXTRA_STREAM, uris); -        } else { -            sendIntent.setAction(Intent.ACTION_SEND); -            sendIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); -        } - -        return sendIntent; -    } - -    private Intent createViewTextIntent() { -        Intent viewIntent = new Intent(); -        viewIntent.setAction(Intent.ACTION_VIEW); -        viewIntent.putExtra(Intent.EXTRA_TEXT, "testing intent viewing"); -        return viewIntent; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE)); -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( -            int numberOfResults, -            UserHandle resolvedForPersonalUser, -            UserHandle resolvedForClonedUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < 1; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                    resolvedForPersonalUser)); -        } -        for (int i = 1; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                    resolvedForClonedUser)); -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( -            int numberOfResults) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            if (i == 0) { -                infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, -                        PERSONAL_USER_HANDLE)); -            } else { -                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                        PERSONAL_USER_HANDLE)); -            } -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( -            int numberOfResults, int userId) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            if (i == 0) { -                infoList.add( -                        ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, -                                PERSONAL_USER_HANDLE)); -            } else { -                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, -                        PERSONAL_USER_HANDLE)); -            } -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId( -            int numberOfResults, int userId) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, -                    PERSONAL_USER_HANDLE)); -        } -        return infoList; -    } - -    private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) { -        Icon icon = Icon.createWithBitmap(createBitmap()); -        String testTitle = "testTitle"; -        List<ChooserTarget> targets = new ArrayList<>(); -        for (int i = 0; i < numberOfResults; i++) { -            ComponentName componentName; -            if (packageName.isEmpty()) { -                componentName = ResolverDataProvider.createComponentName(i); -            } else { -                componentName = new ComponentName(packageName, packageName + ".class"); -            } -            ChooserTarget tempTarget = new ChooserTarget( -                    testTitle + i, -                    icon, -                    (float) (1 - ((i + 1) / 10.0)), -                    componentName, -                    null); -            targets.add(tempTarget); -        } -        return targets; -    } - -    private void waitForIdle() { -        InstrumentationRegistry.getInstrumentation().waitForIdleSync(); -    } - -    private boolean launchActivityWithTimeout(Intent intent, long timeout) -            throws InterruptedException { -        final int initialState = 0; -        final int completedState = 1; -        final int timeoutState = 2; -        final AtomicInteger state = new AtomicInteger(initialState); -        final CountDownLatch cdl = new CountDownLatch(1); - -        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); -        try { -            executor.execute(() -> { -                mActivityRule.launchActivity(intent); -                state.compareAndSet(initialState, completedState); -                cdl.countDown(); -            }); -            executor.schedule( -                    () -> { -                        state.compareAndSet(initialState, timeoutState); -                        cdl.countDown(); -                    }, -                    timeout, -                    TimeUnit.MILLISECONDS); -            cdl.await(); -            return state.get() == completedState; -        } finally { -            executor.shutdownNow(); -        } -    } - -    private Bitmap createBitmap() { -        return createBitmap(200, 200); -    } - -    private Bitmap createWideBitmap() { -        return createWideBitmap(Color.RED); -    } - -    private Bitmap createWideBitmap(int bgColor) { -        WindowManager windowManager = InstrumentationRegistry.getInstrumentation() -                .getTargetContext() -                .getSystemService(WindowManager.class); -        int width = 3000; -        if (windowManager != null) { -            Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); -            width = bounds.width() + 200; -        } -        return createBitmap(width, 100, bgColor); -    } - -    private Bitmap createBitmap(int width, int height) { -        return createBitmap(width, height, Color.RED); -    } - -    private Bitmap createBitmap(int width, int height, int bgColor) { -        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); -        Canvas canvas = new Canvas(bitmap); - -        Paint paint = new Paint(); -        paint.setColor(bgColor); -        paint.setStyle(Paint.Style.FILL); -        canvas.drawPaint(paint); - -        paint.setColor(Color.WHITE); -        paint.setAntiAlias(true); -        paint.setTextSize(14.f); -        paint.setTextAlign(Paint.Align.CENTER); -        canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint); - -        return bitmap; -    } - -    private List<ShareShortcutInfo> createShortcuts(Context context) { -        Intent testIntent = new Intent("TestIntent"); - -        List<ShareShortcutInfo> shortcuts = new ArrayList<>(); -        shortcuts.add(new ShareShortcutInfo( -                new ShortcutInfo.Builder(context, "shortcut1") -                        .setIntent(testIntent).setShortLabel("label1").setRank(3).build(), // 0  2 -                new ComponentName("package1", "class1"))); -        shortcuts.add(new ShareShortcutInfo( -                new ShortcutInfo.Builder(context, "shortcut2") -                        .setIntent(testIntent).setShortLabel("label2").setRank(7).build(), // 1  3 -                new ComponentName("package2", "class2"))); -        shortcuts.add(new ShareShortcutInfo( -                new ShortcutInfo.Builder(context, "shortcut3") -                        .setIntent(testIntent).setShortLabel("label3").setRank(1).build(), // 2  0 -                new ComponentName("package3", "class3"))); -        shortcuts.add(new ShareShortcutInfo( -                new ShortcutInfo.Builder(context, "shortcut4") -                        .setIntent(testIntent).setShortLabel("label4").setRank(3).build(), // 3  2 -                new ComponentName("package4", "class4"))); - -        return shortcuts; -    } - -    private void markWorkProfileUserAvailable() { -        ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); -    } - -    private void markCloneProfileUserAvailable() { -        ChooserActivityOverrideData.getInstance().cloneProfileUserHandle = UserHandle.of(11); -    } - -    private void setupResolverControllers( -            List<ResolvedComponentInfo> personalResolvedComponentInfos) { -        setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); -    } - -    private void setupResolverControllers( -            List<ResolvedComponentInfo> personalResolvedComponentInfos, -            List<ResolvedComponentInfo> workResolvedComponentInfos) { -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .resolverListController -                        .getResolversForIntentAsUser( -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.isA(List.class), -                                eq(UserHandle.SYSTEM))) -                .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .workResolverListController -                        .getResolversForIntentAsUser( -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.isA(List.class), -                                eq(UserHandle.SYSTEM))) -                .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when( -                ChooserActivityOverrideData -                        .getInstance() -                        .workResolverListController -                        .getResolversForIntentAsUser( -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.anyBoolean(), -                                Mockito.isA(List.class), -                                eq(UserHandle.of(10)))) -                .thenReturn(new ArrayList<>(workResolvedComponentInfos)); -    } - -    private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) { -        return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount)); -    } - -    private static class GridRecyclerSpanCountMatcher extends -            BoundedDiagnosingMatcher<View, RecyclerView> { - -        private final Matcher<Integer> mIntegerMatcher; - -        private GridRecyclerSpanCountMatcher(Matcher<Integer> integerMatcher) { -            super(RecyclerView.class); -            this.mIntegerMatcher = integerMatcher; -        } - -        @Override -        protected void describeMoreTo(Description description) { -            description.appendText("RecyclerView grid layout span count to match: "); -            this.mIntegerMatcher.describeTo(description); -        } - -        @Override -        protected boolean matchesSafely(RecyclerView view, Description mismatchDescription) { -            int spanCount = ((GridLayoutManager) view.getLayoutManager()).getSpanCount(); -            if (this.mIntegerMatcher.matches(spanCount)) { -                return true; -            } else { -                mismatchDescription.appendText("RecyclerView grid layout span count was ") -                        .appendValue(spanCount); -                return false; -            } -        } -    } - -    private void givenAppTargets(int appCount) { -        List<ResolvedComponentInfo> resolvedComponentInfos = -                createResolvedComponentsForTest(appCount); -        setupResolverControllers(resolvedComponentInfos); -    } - -    private void updateMaxTargetsPerRowResource(int targetsPerRow) { -        Resources resources = Mockito.spy( -                InstrumentationRegistry.getInstrumentation().getContext().getResources()); -        ChooserActivityOverrideData.getInstance().resources = resources; -        doReturn(targetsPerRow).when(resources).getInteger( -                R.integer.config_chooser_max_targets_per_row); -    } - -    private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> -            createShortcutLoaderFactory() { -        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = -                new SparseArray<>(); -        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = -                (userHandle, callback) -> { -                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = -                            new Pair<>(mock(ShortcutLoader.class), callback); -                    shortcutLoaders.put(userHandle.getIdentifier(), pair); -                    return pair.first; -                }; -        return shortcutLoaders; -    } - -    private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { -        return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); -    } -} diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java deleted file mode 100644 index 92bccb7d..00000000 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * 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; - -import static android.testing.PollingCheck.waitFor; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.swipeUp; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isSelected; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - -import static com.android.intentresolver.ChooserWrapperActivity.sOverrides; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL; -import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK; - -import static org.hamcrest.CoreMatchers.not; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import android.companion.DeviceFilter; -import android.content.Intent; -import android.os.UserHandle; - -import androidx.test.InstrumentationRegistry; -import androidx.test.espresso.NoMatchingViewException; -import androidx.test.rule.ActivityTestRule; - -import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; - -import junit.framework.AssertionFailedError; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.Mockito; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -@DeviceFilter.MediumType -@RunWith(Parameterized.class) -public class UnbundledChooserActivityWorkProfileTest { - -    private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry -            .getInstrumentation().getTargetContext().getUser(); -    private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); - -    @Rule -    public ActivityTestRule<ChooserWrapperActivity> mActivityRule = -            new ActivityTestRule<>(ChooserWrapperActivity.class, false, -                    false); -    private final TestCase mTestCase; - -    public UnbundledChooserActivityWorkProfileTest(TestCase testCase) { -        mTestCase = testCase; -    } - -    @Before -    public void cleanOverrideData() { -        // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the -        // permissions we require (which we'll read from the manifest at runtime). -        InstrumentationRegistry -                .getInstrumentation() -                .getUiAutomation() -                .adoptShellPermissionIdentity(); - -        sOverrides.reset(); -    } - -    @Test -    public void testBlocker() { -        setUpPersonalAndWorkComponentInfos(); -        sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); -        sOverrides.tabOwnerUserHandleForLaunch = mTestCase.getMyUserHandle(); - -        launchActivity(mTestCase.getIsSendAction()); -        switchToTab(mTestCase.getTab()); - -        switch (mTestCase.getExpectedBlocker()) { -            case NO_BLOCKER: -                assertNoBlockerDisplayed(); -                break; -            case PERSONAL_PROFILE_SHARE_BLOCKER: -                assertCantSharePersonalAppsBlockerDisplayed(); -                break; -            case WORK_PROFILE_SHARE_BLOCKER: -                assertCantShareWorkAppsBlockerDisplayed(); -                break; -            case PERSONAL_PROFILE_ACCESS_BLOCKER: -                assertCantAccessPersonalAppsBlockerDisplayed(); -                break; -            case WORK_PROFILE_ACCESS_BLOCKER: -                assertCantAccessWorkAppsBlockerDisplayed(); -                break; -        } -    } - -    @Parameterized.Parameters(name = "{0}") -    public static Collection tests() { -        return Arrays.asList( -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ true, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ WORK, -                        /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ WORK_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ true, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ), -                new TestCase( -                        /* isSendAction= */ false, -                        /* hasCrossProfileIntents= */ false, -                        /* myUserHandle= */ PERSONAL_USER_HANDLE, -                        /* tab= */ PERSONAL, -                        /* expectedBlocker= */ NO_BLOCKER -                ) -        ); -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( -            int numberOfResults, int userId, UserHandle resolvedForUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            infoList.add( -                    ResolverDataProvider -                            .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser)); -        } -        return infoList; -    } - -    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, -            UserHandle resolvedForUser) { -        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); -        for (int i = 0; i < numberOfResults; i++) { -            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); -        } -        return infoList; -    } - -    private void setUpPersonalAndWorkComponentInfos() { -        markWorkProfileUserAvailable(); -        int workProfileTargets = 4; -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, -                        /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -    } - -    private void setupResolverControllers( -            List<ResolvedComponentInfo> personalResolvedComponentInfos, -            List<ResolvedComponentInfo> workResolvedComponentInfos) { -        when(sOverrides.resolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(UserHandle.SYSTEM))) -                        .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when(sOverrides.workResolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(UserHandle.SYSTEM))) -                        .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); -        when(sOverrides.workResolverListController.getResolversForIntentAsUser( -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.anyBoolean(), -                Mockito.isA(List.class), -                eq(WORK_USER_HANDLE))) -                        .thenReturn(new ArrayList<>(workResolvedComponentInfos)); -    } - -    private void waitForIdle() { -        InstrumentationRegistry.getInstrumentation().waitForIdleSync(); -    } - -    private void markWorkProfileUserAvailable() { -        ChooserWrapperActivity.sOverrides.workProfileUserHandle = WORK_USER_HANDLE; -    } - -    private void assertCantAccessWorkAppsBlockerDisplayed() { -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -        onView(withText(R.string.resolver_cant_access_work_apps_explanation)) -                .check(matches(isDisplayed())); -    } - -    private void assertCantAccessPersonalAppsBlockerDisplayed() { -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -        onView(withText(R.string.resolver_cant_access_personal_apps_explanation)) -                .check(matches(isDisplayed())); -    } - -    private void assertCantShareWorkAppsBlockerDisplayed() { -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -        onView(withText(R.string.resolver_cant_share_with_work_apps_explanation)) -                .check(matches(isDisplayed())); -    } - -    private void assertCantSharePersonalAppsBlockerDisplayed() { -        onView(withText(R.string.resolver_cross_profile_blocked)) -                .check(matches(isDisplayed())); -        onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation)) -                .check(matches(isDisplayed())); -    } - -    private void assertNoBlockerDisplayed() { -        try { -            onView(withText(R.string.resolver_cross_profile_blocked)) -                    .check(matches(not(isDisplayed()))); -        } catch (NoMatchingViewException ignored) { -        } -    } - -    private void switchToTab(Tab tab) { -        final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab -                : R.string.resolver_personal_tab; - -        waitFor(() -> { -            onView(withText(stringId)).perform(click()); -            waitForIdle(); - -            try { -                onView(withText(stringId)).check(matches(isSelected())); -                return true; -            } catch (AssertionFailedError e) { -                return false; -            } -        }); - -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        waitForIdle(); -    } - -    private Intent createTextIntent(boolean isSendAction) { -        Intent sendIntent = new Intent(); -        if (isSendAction) { -            sendIntent.setAction(Intent.ACTION_SEND); -        } -        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); -        sendIntent.setType("text/plain"); -        return sendIntent; -    } - -    private void launchActivity(boolean isSendAction) { -        Intent sendIntent = createTextIntent(isSendAction); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); -        waitForIdle(); -    } - -    public static class TestCase { -        private final boolean mIsSendAction; -        private final boolean mHasCrossProfileIntents; -        private final UserHandle mMyUserHandle; -        private final Tab mTab; -        private final ExpectedBlocker mExpectedBlocker; - -        public enum ExpectedBlocker { -            NO_BLOCKER, -            PERSONAL_PROFILE_SHARE_BLOCKER, -            WORK_PROFILE_SHARE_BLOCKER, -            PERSONAL_PROFILE_ACCESS_BLOCKER, -            WORK_PROFILE_ACCESS_BLOCKER -        } - -        public enum Tab { -            WORK, -            PERSONAL -        } - -        public TestCase(boolean isSendAction, boolean hasCrossProfileIntents, -                UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) { -            mIsSendAction = isSendAction; -            mHasCrossProfileIntents = hasCrossProfileIntents; -            mMyUserHandle = myUserHandle; -            mTab = tab; -            mExpectedBlocker = expectedBlocker; -        } - -        public boolean getIsSendAction() { -            return mIsSendAction; -        } - -        public boolean hasCrossProfileIntents() { -            return mHasCrossProfileIntents; -        } - -        public UserHandle getMyUserHandle() { -            return mMyUserHandle; -        } - -        public Tab getTab() { -            return mTab; -        } - -        public ExpectedBlocker getExpectedBlocker() { -            return mExpectedBlocker; -        } - -        @Override -        public String toString() { -            StringBuilder result = new StringBuilder("test"); - -            if (mTab == WORK) { -                result.append("WorkTab_"); -            } else { -                result.append("PersonalTab_"); -            } - -            if (mIsSendAction) { -                result.append("sendAction_"); -            } else { -                result.append("notSendAction_"); -            } - -            if (mHasCrossProfileIntents) { -                result.append("hasCrossProfileIntents_"); -            } else { -                result.append("doesNotHaveCrossProfileIntents_"); -            } - -            if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) { -                result.append("myUserIsPersonal_"); -            } else { -                result.append("myUserIsWork_"); -            } - -            if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) { -                result.append("thenNoBlocker"); -            } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) { -                result.append("thenAccessBlockerOnPersonalProfile"); -            } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) { -                result.append("thenShareBlockerOnPersonalProfile"); -            } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) { -                result.append("thenAccessBlockerOnWorkProfile"); -            } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) { -                result.append("thenShareBlockerOnWorkProfile"); -            } - -            return result.toString(); -        } -    } -} diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt deleted file mode 100644 index f3ca76a9..00000000 --- a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt +++ /dev/null @@ -1,503 +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 - *3 - * 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.chooser - -import android.app.Activity -import android.app.prediction.AppTarget -import android.app.prediction.AppTargetId -import android.content.ComponentName -import android.content.Intent -import android.content.pm.ResolveInfo -import android.os.Bundle -import android.os.UserHandle -import com.android.intentresolver.createShortcutInfo -import com.android.intentresolver.mock -import com.android.intentresolver.ResolverActivity -import com.android.intentresolver.ResolverDataProvider -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import androidx.test.platform.app.InstrumentationRegistry - -class ImmutableTargetInfoTest { -    private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry -            .getInstrumentation().getTargetContext().getUser() - -    private val resolvedIntent = Intent("resolved") -    private val targetIntent = Intent("target") -    private val referrerFillInIntent = Intent("referrer_fillin") -    private val resolvedComponentName = ComponentName("resolved", "component") -    private val chooserTargetComponentName = ComponentName("chooser", "target") -    private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE) -    private val displayLabel: CharSequence = "Display Label" -    private val extendedInfo: CharSequence = "Extended Info" -    private val displayIconHolder: TargetInfo.IconHolder = mock() -    private val sourceIntent1 = Intent("source1") -    private val sourceIntent2 = Intent("source2") -    private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo( -        Intent("display1"), -        ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE), -        "display1 label", -        "display1 extended info", -        Intent("display1_resolved"), -        /* resolveInfoPresentationGetter= */ null) -    private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo( -        Intent("display2"), -        ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), -        "display2 label", -        "display2 extended info", -        Intent("display2_resolved"), -        /* resolveInfoPresentationGetter= */ null) -    private val directShareShortcutInfo = createShortcutInfo( -        "shortcutid", ResolverDataProvider.createComponentName(4), 4) -    private val directShareAppTarget = AppTarget( -        AppTargetId("apptargetid"), -        "test.directshare", -        "target", -        UserHandle.CURRENT) -    private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( -        Intent("displayresolve"), -        ResolverDataProvider.createResolveInfo(5, 0, PERSONAL_USER_HANDLE), -        "displayresolve label", -        "displayresolve extended info", -        Intent("display_resolved"), -        /* resolveInfoPresentationGetter= */ null) -    private val hashProvider: ImmutableTargetInfo.TargetHashProvider = mock() - -    @Test -    fun testBasicProperties() {  // Fields that are reflected back w/o logic. -        // TODO: we could consider passing copies of all the values into the builder so that we can -        // verify that they're not mutated (e.g. no extras added to the intents). For now that -        // should be obvious from the implementation. -        val info = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(resolvedIntent) -            .setTargetIntent(targetIntent) -            .setReferrerFillInIntent(referrerFillInIntent) -            .setResolvedComponentName(resolvedComponentName) -            .setChooserTargetComponentName(chooserTargetComponentName) -            .setResolveInfo(resolveInfo) -            .setDisplayLabel(displayLabel) -            .setExtendedInfo(extendedInfo) -            .setDisplayIconHolder(displayIconHolder) -            .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2)) -            .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) -            .setIsSuspended(true) -            .setIsPinned(true) -            .setModifiedScore(42.0f) -            .setDirectShareShortcutInfo(directShareShortcutInfo) -            .setDirectShareAppTarget(directShareAppTarget) -            .setDisplayResolveInfo(displayResolveInfo) -            .setHashProvider(hashProvider) -            .build() - -        assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) -        assertThat(info.targetIntent).isEqualTo(targetIntent) -        assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) -        assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) -        assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) -        assertThat(info.resolveInfo).isEqualTo(resolveInfo) -        assertThat(info.displayLabel).isEqualTo(displayLabel) -        assertThat(info.extendedInfo).isEqualTo(extendedInfo) -        assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) -        assertThat(info.allSourceIntents).containsExactly( -            resolvedIntent, sourceIntent1, sourceIntent2) -        assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) -        assertThat(info.isSuspended).isTrue() -        assertThat(info.isPinned).isTrue() -        assertThat(info.modifiedScore).isEqualTo(42.0f) -        assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) -        assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) -        assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isFalse() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isFalse() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isFalse() -        assertThat(info.hashProvider).isEqualTo(hashProvider) -    } - -    @Test -    fun testToBuilderPreservesBasicProperties() { -        // Note this is set up exactly as in `testBasicProperties`, but the assertions will be made -        // against a *copy* of the object instead. -        val infoToCopyFrom = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(resolvedIntent) -            .setTargetIntent(targetIntent) -            .setReferrerFillInIntent(referrerFillInIntent) -            .setResolvedComponentName(resolvedComponentName) -            .setChooserTargetComponentName(chooserTargetComponentName) -            .setResolveInfo(resolveInfo) -            .setDisplayLabel(displayLabel) -            .setExtendedInfo(extendedInfo) -            .setDisplayIconHolder(displayIconHolder) -            .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2)) -            .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) -            .setIsSuspended(true) -            .setIsPinned(true) -            .setModifiedScore(42.0f) -            .setDirectShareShortcutInfo(directShareShortcutInfo) -            .setDirectShareAppTarget(directShareAppTarget) -            .setDisplayResolveInfo(displayResolveInfo) -            .setHashProvider(hashProvider) -            .build() - -        val info = infoToCopyFrom.toBuilder().build() - -        assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) -        assertThat(info.targetIntent).isEqualTo(targetIntent) -        assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) -        assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) -        assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) -        assertThat(info.resolveInfo).isEqualTo(resolveInfo) -        assertThat(info.displayLabel).isEqualTo(displayLabel) -        assertThat(info.extendedInfo).isEqualTo(extendedInfo) -        assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) -        assertThat(info.allSourceIntents).containsExactly( -            resolvedIntent, sourceIntent1, sourceIntent2) -        assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) -        assertThat(info.isSuspended).isTrue() -        assertThat(info.isPinned).isTrue() -        assertThat(info.modifiedScore).isEqualTo(42.0f) -        assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) -        assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) -        assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isFalse() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isFalse() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isFalse() -        assertThat(info.hashProvider).isEqualTo(hashProvider) -    } - -    @Test -    fun testBaseIntentToSend_defaultsToResolvedIntent() { -        val info = ImmutableTargetInfo.newBuilder().setResolvedIntent(resolvedIntent).build() -        assertThat(info.baseIntentToSend.filterEquals(resolvedIntent)).isTrue() -    } - -    @Test -    fun testBaseIntentToSend_fillsInFromReferrerIntent() { -        val originalIntent = Intent() -        originalIntent.setPackage("original") - -        val referrerFillInIntent = Intent("REFERRER_FILL_IN") -        referrerFillInIntent.setPackage("referrer") - -        val info = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .setReferrerFillInIntent(referrerFillInIntent) -            .build() - -        assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original")  // Only fill if empty. -        assertThat(info.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN") -    } - -    @Test -    fun testBaseIntentToSend_fillsInFromRefinementIntent() { -        val originalIntent = Intent() -        originalIntent.putExtra("ORIGINAL", true) - -        val refinementIntent = Intent() -        refinementIntent.putExtra("REFINEMENT", true) - -        val originalInfo = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .build() -        val info = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinementIntent)) - -        assertThat(info?.baseIntentToSend?.getBooleanExtra("ORIGINAL", false)).isTrue() -        assertThat(info?.baseIntentToSend?.getBooleanExtra("REFINEMENT", false)).isTrue() -    } - -    @Test -    fun testBaseIntentToSend_twoFillInSourcesFavorsRefinementRequest() { -        val originalIntent = Intent("REFINE_ME") -        originalIntent.setPackage("original") - -        val referrerFillInIntent = Intent("REFERRER_FILL_IN") -        referrerFillInIntent.setPackage("referrer_pkg") -        referrerFillInIntent.setType("test/referrer") - -        val infoWithReferrerFillIn = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .setReferrerFillInIntent(referrerFillInIntent) -            .build() - -        val refinementIntent = Intent("REFINE_ME") -        refinementIntent.setPackage("original")  // Has to match for refinement. - -        val info = -            checkNotNull(infoWithReferrerFillIn.tryToCloneWithAppliedRefinement(refinementIntent)) - -        assertThat(info?.baseIntentToSend?.getPackage()).isEqualTo("original")  // Set all along. -        assertThat(info?.baseIntentToSend?.action).isEqualTo("REFINE_ME")  // Refinement wins. -        assertThat(info?.baseIntentToSend?.type).isEqualTo("test/referrer")  // Left for referrer. -    } - -    @Test -    fun testBaseIntentToSend_doubleRefinementPreservesReferrerFillInButNotOriginalRefinement() { -        val originalIntent = Intent("REFINE_ME") -        val referrerFillInIntent = Intent("REFERRER_FILL_IN") -        referrerFillInIntent.putExtra("TEST", "REFERRER") -        val refinementIntent1 = Intent("REFINE_ME") -        refinementIntent1.putExtra("TEST1", "1") -        val refinementIntent2 = Intent("REFINE_ME") -        refinementIntent2.putExtra("TEST2", "2") - -        val originalInfo = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .setReferrerFillInIntent(referrerFillInIntent) -            .build() - -        val refined1 = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinementIntent1)) -        // Cloned clone. -        val refined2 = checkNotNull(refined1.tryToCloneWithAppliedRefinement(refinementIntent2)) - -        // Both clones get the same values filled in from the referrer intent. -        assertThat(refined1?.baseIntentToSend?.getStringExtra("TEST")).isEqualTo("REFERRER") -        assertThat(refined2?.baseIntentToSend?.getStringExtra("TEST")).isEqualTo("REFERRER") -        // Each clone has the respective value that was set in their own refinement request. -        assertThat(refined1?.baseIntentToSend?.getStringExtra("TEST1")).isEqualTo("1") -        assertThat(refined2?.baseIntentToSend?.getStringExtra("TEST2")).isEqualTo("2") -        // The clones don't have the data from each other's refinements, even though the intent -        // field is empty (thus able to be populated by filling-in). -        assertThat(refined1?.baseIntentToSend?.getStringExtra("TEST2")).isNull() -        assertThat(refined2?.baseIntentToSend?.getStringExtra("TEST1")).isNull() -    } - -    @Test -    fun testBaseIntentToSend_refinementToAlternateSourceIntent() { -        val originalIntent = Intent("DONT_REFINE_ME") -        originalIntent.putExtra("originalIntent", true) -        val mismatchedAlternate = Intent("DOESNT_MATCH") -        mismatchedAlternate.putExtra("mismatchedAlternate", true) -        val targetAlternate = Intent("REFINE_ME") -        targetAlternate.putExtra("targetAlternate", true) -        val extraMatch = Intent("REFINE_ME") -        extraMatch.putExtra("extraMatch", true) - -        val originalInfo = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .setAllSourceIntents(listOf( -                    originalIntent, mismatchedAlternate, targetAlternate, extraMatch)) -            .build() - -        val refinement = Intent("REFINE_ME")  // First match is `targetAlternate` -        refinement.putExtra("refinement", true) - -        val refinedResult = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinement)) -        assertThat(refinedResult?.baseIntentToSend?.getBooleanExtra("refinement", false)).isTrue() -        assertThat(refinedResult?.baseIntentToSend?.getBooleanExtra("targetAlternate", false)) -            .isTrue() -        // None of the other source intents got merged in (not even the later one that matched): -        assertThat(refinedResult?.baseIntentToSend?.getBooleanExtra("originalIntent", false)) -            .isFalse() -        assertThat(refinedResult?.baseIntentToSend?.getBooleanExtra("mismatchedAlternate", false)) -            .isFalse() -        assertThat(refinedResult?.baseIntentToSend?.getBooleanExtra("extraMatch", false)).isFalse() -    } - -    @Test -    fun testBaseIntentToSend_noSourceIntentMatchingProposedRefinement() { -        val originalIntent = Intent("DONT_REFINE_ME") -        originalIntent.putExtra("originalIntent", true) -        val mismatchedAlternate = Intent("DOESNT_MATCH") -        mismatchedAlternate.putExtra("mismatchedAlternate", true) - -        val originalInfo = ImmutableTargetInfo.newBuilder() -            .setResolvedIntent(originalIntent) -            .setAllSourceIntents(listOf(originalIntent, mismatchedAlternate)) -            .build() - -        val refinement = Intent("PROPOSED_REFINEMENT") -        assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() -    } - -    @Test -    fun testLegacySubclassRelationships_empty() { -        val info = ImmutableTargetInfo.newBuilder() -            .setLegacyType(ImmutableTargetInfo.LegacyTargetType.EMPTY_TARGET_INFO) -            .build() - -        assertThat(info.isEmptyTargetInfo).isTrue() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isTrue() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isTrue() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isFalse() -    } - -    @Test -    fun testLegacySubclassRelationships_placeholder() { -        val info = ImmutableTargetInfo.newBuilder() -            .setLegacyType(ImmutableTargetInfo.LegacyTargetType.PLACEHOLDER_TARGET_INFO) -            .build() - -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isTrue() -        assertThat(info.isNotSelectableTargetInfo).isTrue() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isTrue() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isFalse() -    } - -    @Test -    fun testLegacySubclassRelationships_selectable() { -        val info = ImmutableTargetInfo.newBuilder() -            .setLegacyType(ImmutableTargetInfo.LegacyTargetType.SELECTABLE_TARGET_INFO) -            .build() - -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isFalse() -        assertThat(info.isSelectableTargetInfo).isTrue() -        assertThat(info.isChooserTargetInfo).isTrue() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isFalse() -    } - -    @Test -    fun testLegacySubclassRelationships_displayResolveInfo() { -        val info = ImmutableTargetInfo.newBuilder() -            .setLegacyType(ImmutableTargetInfo.LegacyTargetType.DISPLAY_RESOLVE_INFO) -            .build() - -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isFalse() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isFalse() -        assertThat(info.isMultiDisplayResolveInfo).isFalse() -        assertThat(info.isDisplayResolveInfo).isTrue() -    } - -    @Test -    fun testLegacySubclassRelationships_multiDisplayResolveInfo() { -        val info = ImmutableTargetInfo.newBuilder() -            .setLegacyType(ImmutableTargetInfo.LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO) -            .build() - -        assertThat(info.isEmptyTargetInfo).isFalse() -        assertThat(info.isPlaceHolderTargetInfo).isFalse() -        assertThat(info.isNotSelectableTargetInfo).isFalse() -        assertThat(info.isSelectableTargetInfo).isFalse() -        assertThat(info.isChooserTargetInfo).isFalse() -        assertThat(info.isMultiDisplayResolveInfo).isTrue() -        assertThat(info.isDisplayResolveInfo).isTrue() -    } - -    @Test -    fun testActivityStarter_correctNumberOfInvocations_startAsCaller() { -        val activityStarter = object : TestActivityStarter() { -            override fun startAsUser( -                target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle -            ): Boolean { -                throw RuntimeException("Wrong API used: startAsUser") -            } -        } - -        val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() -        val activity: ResolverActivity = mock() -        val options = Bundle() -        options.putInt("TEST_KEY", 1) - -        info.startAsCaller(activity, options, 42) - -        assertThat(activityStarter.totalInvocations).isEqualTo(1) -        assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) -        assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) -        assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) -        assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) -        assertThat(activityStarter.lastInvocationAsCaller).isTrue() -    } - -    @Test -    fun testActivityStarter_correctNumberOfInvocations_startAsUser() { -        val activityStarter = object : TestActivityStarter() { -            override fun startAsCaller( -                target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { -                throw RuntimeException("Wrong API used: startAsCaller") -            } -        } - -        val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() -        val activity: Activity = mock() -        val options = Bundle() -        options.putInt("TEST_KEY", 1) - -        info.startAsUser(activity, options, UserHandle.of(42)) - -        assertThat(activityStarter.totalInvocations).isEqualTo(1) -        assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) -        assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) -        assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) -        assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) -        assertThat(activityStarter.lastInvocationAsCaller).isFalse() -    } - -    @Test -    fun testActivityStarter_invokedWithRespectiveTargetInfoAfterCopy() { -        val activityStarter = TestActivityStarter() -        val info1 = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() -        val info2 = info1.toBuilder().build() - -        info1.startAsCaller(mock(), Bundle(), 42) -        assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info1) -        info2.startAsCaller(mock(), Bundle(), 42) -        assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) -        info2.startAsUser(mock(), Bundle(), UserHandle.of(42)) -        assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) - -        assertThat(activityStarter.totalInvocations).isEqualTo(3)  // Instance is still shared. -    } -} - -private open class TestActivityStarter : ImmutableTargetInfo.TargetActivityStarter { -    var totalInvocations = 0 -    var lastInvocationTargetInfo: TargetInfo? = null -    var lastInvocationActivity: Activity? = null -    var lastInvocationOptions: Bundle? = null -    var lastInvocationUserId: Integer? = null -    var lastInvocationAsCaller = false - -    override fun startAsCaller( -            target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { -        ++totalInvocations -        lastInvocationTargetInfo = target -        lastInvocationActivity = activity -        lastInvocationOptions = options -        lastInvocationUserId = Integer(userId) -        lastInvocationAsCaller = true -        return true -    } - -    override fun startAsUser( -            target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle): Boolean { -        ++totalInvocations -        lastInvocationTargetInfo = target -        lastInvocationActivity = activity -        lastInvocationOptions = options -        lastInvocationUserId = Integer(user.identifier) -        lastInvocationAsCaller = false -        return true -    } -} diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt deleted file mode 100644 index 78e0c3ee..00000000 --- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt +++ /dev/null @@ -1,399 +0,0 @@ -/* - * 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.chooser - -import android.app.prediction.AppTarget -import android.app.prediction.AppTargetId -import android.content.ComponentName -import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo -import android.graphics.drawable.AnimatedVectorDrawable -import android.os.UserHandle -import android.test.UiThreadTest -import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.ResolverDataProvider -import com.android.intentresolver.createChooserTarget -import com.android.intentresolver.createShortcutInfo -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.any -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -class TargetInfoTest { -    private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry -            .getInstrumentation().getTargetContext().getUser() - -    private val context = InstrumentationRegistry.getInstrumentation().getContext() - -    @Before -    fun setup() { -        // SelectableTargetInfo reads DeviceConfig and needs a permission for that. -        InstrumentationRegistry -            .getInstrumentation() -            .getUiAutomation() -            .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") -    } - -    @Test -    fun testNewEmptyTargetInfo() { -        val info = NotSelectableTargetInfo.newEmptyTargetInfo() -        assertThat(info.isEmptyTargetInfo()).isTrue() -        assertThat(info.isChooserTargetInfo()).isTrue()  // From legacy inheritance model. -        assertThat(info.hasDisplayIcon()).isFalse() -        assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull() -    } - -    @UiThreadTest  // AnimatedVectorDrawable needs to start from a thread with a Looper. -    @Test -    fun testNewPlaceholderTargetInfo() { -        val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context) -        assertThat(info.isPlaceHolderTargetInfo).isTrue() -        assertThat(info.isChooserTargetInfo).isTrue()  // From legacy inheritance model. -        assertThat(info.hasDisplayIcon()).isTrue() -        assertThat(info.displayIconHolder.displayIcon) -                .isInstanceOf(AnimatedVectorDrawable::class.java) -        // TODO: assert that the animation is pre-started/running (IIUC this requires synchronizing -        // with some "render thread" per the `AnimatedVectorDrawable` docs). I believe this is -        // possible using `AnimatorTestRule` but I couldn't find any sample usage in Kotlin nor get -        // it working myself. -    } - -    @Test -    fun testNewSelectableTargetInfo() { -        val resolvedIntent = Intent() -        val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( -            resolvedIntent, -            ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE), -            "label", -            "extended info", -            resolvedIntent, -            /* resolveInfoPresentationGetter= */ null) -        val chooserTarget = createChooserTarget( -            "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id") -        val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) -        val appTarget = AppTarget( -            AppTargetId("id"), -            chooserTarget.componentName.packageName, -            chooserTarget.componentName.className, -            UserHandle.CURRENT) - -        val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( -            baseDisplayInfo, -            mock(), -            resolvedIntent, -            chooserTarget, -            0.1f, -            shortcutInfo, -            appTarget, -            mock(), -        ) -        assertThat(targetInfo.isSelectableTargetInfo).isTrue() -        assertThat(targetInfo.isChooserTargetInfo).isTrue()  // From legacy inheritance model. -        assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(baseDisplayInfo) -        assertThat(targetInfo.chooserTargetComponentName).isEqualTo(chooserTarget.componentName) -        assertThat(targetInfo.directShareShortcutId).isEqualTo(shortcutInfo.id) -        assertThat(targetInfo.directShareShortcutInfo).isSameInstanceAs(shortcutInfo) -        assertThat(targetInfo.directShareAppTarget).isSameInstanceAs(appTarget) -        assertThat(targetInfo.resolvedIntent).isSameInstanceAs(resolvedIntent) -        // TODO: make more meaningful assertions about the behavior of a selectable target. -    } - -    @Test -    fun test_SelectableTargetInfo_componentName_no_source_info() { -        val chooserTarget = createChooserTarget( -            "title", 0.3f, ResolverDataProvider.createComponentName(1), "test_shortcut_id") -        val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(2), 3) -        val appTarget = AppTarget( -            AppTargetId("id"), -            chooserTarget.componentName.packageName, -            chooserTarget.componentName.className, -            UserHandle.CURRENT) -        val pkgName = "org.package" -        val className = "MainActivity" -        val backupResolveInfo = ResolveInfo().apply { -            activityInfo = ActivityInfo().apply { -                packageName = pkgName -                name = className -            } -        } - -        val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( -            null, -            backupResolveInfo, -            mock(), -            chooserTarget, -            0.1f, -            shortcutInfo, -            appTarget, -            mock(), -        ) -        assertThat(targetInfo.resolvedComponentName).isEqualTo(ComponentName(pkgName, className)) -    } - -    @Test -    fun testSelectableTargetInfo_noSourceIntentMatchingProposedRefinement() { -        val resolvedIntent = Intent("DONT_REFINE_ME") -        resolvedIntent.putExtra("resolvedIntent", true) - -        val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( -            resolvedIntent, -            ResolverDataProvider.createResolveInfo(1, 0), -            "label", -            "extended info", -            resolvedIntent, -            /* resolveInfoPresentationGetter= */ null) -        val chooserTarget = createChooserTarget( -            "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id") -        val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) -        val appTarget = AppTarget( -            AppTargetId("id"), -            chooserTarget.componentName.packageName, -            chooserTarget.componentName.className, -            UserHandle.CURRENT) - -        val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( -            baseDisplayInfo, -            mock(), -            resolvedIntent, -            chooserTarget, -            0.1f, -            shortcutInfo, -            appTarget, -            mock(), -        ) - -        val refinement = Intent("PROPOSED_REFINEMENT") -        assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() -    } - -    @Test -    fun testNewDisplayResolveInfo() { -        val intent = Intent(Intent.ACTION_SEND) -        intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") -        intent.setType("text/plain") - -        val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) - -        val targetInfo = DisplayResolveInfo.newDisplayResolveInfo( -            intent, -            resolveInfo, -            "label", -            "extended info", -            intent, -            /* resolveInfoPresentationGetter= */ null) -        assertThat(targetInfo.isDisplayResolveInfo()).isTrue() -        assertThat(targetInfo.isMultiDisplayResolveInfo()).isFalse() -        assertThat(targetInfo.isChooserTargetInfo()).isFalse() -    } - -    @Test -    fun test_DisplayResolveInfo_refinementToAlternateSourceIntent() { -        val originalIntent = Intent("DONT_REFINE_ME") -        originalIntent.putExtra("originalIntent", true) -        val mismatchedAlternate = Intent("DOESNT_MATCH") -        mismatchedAlternate.putExtra("mismatchedAlternate", true) -        val targetAlternate = Intent("REFINE_ME") -        targetAlternate.putExtra("targetAlternate", true) -        val extraMatch = Intent("REFINE_ME") -        extraMatch.putExtra("extraMatch", true) - -        val originalInfo = DisplayResolveInfo.newDisplayResolveInfo( -            originalIntent, -            ResolverDataProvider.createResolveInfo(3, 0), -            "label", -            "extended info", -            originalIntent, -            /* resolveInfoPresentationGetter= */ null) -        originalInfo.addAlternateSourceIntent(mismatchedAlternate) -        originalInfo.addAlternateSourceIntent(targetAlternate) -        originalInfo.addAlternateSourceIntent(extraMatch) - -        val refinement = Intent("REFINE_ME")  // First match is `targetAlternate` -        refinement.putExtra("refinement", true) - -        val refinedResult = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinement)) -        // Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`. -        assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("refinement", false)).isTrue() -        assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("targetAlternate", false)) -            .isTrue() -        // None of the other source intents got merged in (not even the later one that matched): -        assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("originalIntent", false)) -            .isFalse() -        assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("mismatchedAlternate", false)) -            .isFalse() -        assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("extraMatch", false)).isFalse() -    } - -    @Test -    fun testDisplayResolveInfo_noSourceIntentMatchingProposedRefinement() { -        val originalIntent = Intent("DONT_REFINE_ME") -        originalIntent.putExtra("originalIntent", true) -        val mismatchedAlternate = Intent("DOESNT_MATCH") -        mismatchedAlternate.putExtra("mismatchedAlternate", true) - -        val originalInfo = DisplayResolveInfo.newDisplayResolveInfo( -            originalIntent, -            ResolverDataProvider.createResolveInfo(3, 0), -            "label", -            "extended info", -            originalIntent, -            /* resolveInfoPresentationGetter= */ null) -        originalInfo.addAlternateSourceIntent(mismatchedAlternate) - -        val refinement = Intent("PROPOSED_REFINEMENT") -        assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() -    } - -    @Test -    fun testNewMultiDisplayResolveInfo() { -        val intent = Intent(Intent.ACTION_SEND) -        intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") -        intent.setType("text/plain") - -        val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) -        val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( -            intent, -            resolveInfo, -            "label 1", -            "extended info 1", -            intent, -            /* resolveInfoPresentationGetter= */ null) -        val secondTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( -            intent, -            resolveInfo, -            "label 2", -            "extended info 2", -            intent, -            /* resolveInfoPresentationGetter= */ null) - -        val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( -            listOf(firstTargetInfo, secondTargetInfo)) - -        assertThat(multiTargetInfo.isMultiDisplayResolveInfo()).isTrue() -        assertThat(multiTargetInfo.isDisplayResolveInfo()).isTrue()  // From legacy inheritance. -        assertThat(multiTargetInfo.isChooserTargetInfo()).isFalse() - -        assertThat(multiTargetInfo.getExtendedInfo()).isNull() - -        assertThat(multiTargetInfo.getAllDisplayTargets()) -                .containsExactly(firstTargetInfo, secondTargetInfo) - -        assertThat(multiTargetInfo.hasSelected()).isFalse() -        assertThat(multiTargetInfo.getSelectedTarget()).isNull() - -        multiTargetInfo.setSelected(1) - -        assertThat(multiTargetInfo.hasSelected()).isTrue() -        assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo) - -        val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent) -        assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java) -        assertThat((refined as MultiDisplayResolveInfo).hasSelected()) -            .isEqualTo(multiTargetInfo.hasSelected()) - -        // TODO: consider exercising activity-start behavior. -        // TODO: consider exercising DisplayResolveInfo base class behavior. -    } - -    @Test -    fun testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTarget() { -        val sendImage = Intent("SEND").apply { type = "image/png" } -        val sendUri = Intent("SEND").apply { type = "text/uri" } - -        val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0) - -        val imageOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo( -            sendImage, -            resolveInfo, -            "Send Image", -            "Sends only images", -            sendImage, -            /* resolveInfoPresentationGetter= */ null) - -        val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo( -            sendUri, -            resolveInfo, -            "Send Text", -            "Sends only text", -            sendUri, -            /* resolveInfoPresentationGetter= */ null) - -        val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo( -            sendImage, -            resolveInfo, -            "Send Image or Text", -            "Sends images or text", -            sendImage, -            /* resolveInfoPresentationGetter= */ null -        ).apply { -            addAlternateSourceIntent(sendUri) -        } - -        val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( -            listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget) -        ) - -        multiTargetInfo.setSelected(0) -        assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget) -        assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOnlyTarget.allSourceIntents) - -        multiTargetInfo.setSelected(1) -        assertThat(multiTargetInfo.selectedTarget).isEqualTo(textOnlyTarget) -        assertThat(multiTargetInfo.allSourceIntents).isEqualTo(textOnlyTarget.allSourceIntents) - -        multiTargetInfo.setSelected(2) -        assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOrTextTarget) -        assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOrTextTarget.allSourceIntents) -    } - -    @Test -    fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() { -        val refined = Intent("SEND") -        val sendImage = Intent("SEND") -        val targetOne = spy( -            DisplayResolveInfo.newDisplayResolveInfo( -                sendImage, -                ResolverDataProvider.createResolveInfo(1, 0), -                "Target One", -                "Target One", -                sendImage, -                /* resolveInfoPresentationGetter= */ null -            ) -        ) -        val targetTwo = mock<DisplayResolveInfo> { -            whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this) -        } - -        val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( -            listOf(targetOne, targetTwo) -        ) - -        multiTargetInfo.setSelected(1) -        assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo) - -        multiTargetInfo.tryToCloneWithAppliedRefinement(refined) -        verify(targetTwo, times(1)).tryToCloneWithAppliedRefinement(refined) -        verify(targetOne, never()).tryToCloneWithAppliedRefinement(any()) -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt deleted file mode 100644 index dab1a956..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ /dev/null @@ -1,149 +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.contentpreview - -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.testing.TestLifecycleOwner -import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.android.intentresolver.widget.ActionRow -import com.android.intentresolver.widget.ImagePreviewView -import com.google.common.truth.Truth.assertThat -import java.util.function.Consumer -import kotlinx.coroutines.flow.MutableSharedFlow -import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -class ChooserContentPreviewUiTest { -    private val lifecycleOwner = TestLifecycleOwner() -    private val previewData = mock<PreviewDataProvider>() -    private val headlineGenerator = mock<HeadlineGenerator>() -    private val imageLoader = -        object : ImageLoader { -            override fun loadImage( -                callerLifecycle: Lifecycle, -                uri: Uri, -                callback: Consumer<Bitmap?>, -            ) { -                callback.accept(null) -            } -            override fun prePopulate(uris: List<Uri>) = Unit -            override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = null -        } -    private val actionFactory = -        object : ActionFactory { -            override fun getCopyButtonRunnable(): Runnable? = null -            override fun getEditButtonRunnable(): Runnable? = null -            override fun createCustomActions(): List<ActionRow.Action> = emptyList() -            override fun getModifyShareAction(): ActionRow.Action? = null -            override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} -        } -    private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>() - -    @Test -    fun test_textPreviewType_useTextPreviewUi() { -        whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT) -        val testSubject = -            ChooserContentPreviewUi( -                lifecycleOwner.lifecycle, -                previewData, -                Intent(Intent.ACTION_VIEW), -                imageLoader, -                actionFactory, -                transitionCallback, -                headlineGenerator, -            ) -        assertThat(testSubject.preferredContentPreview) -            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) -        assertThat(testSubject.mContentPreviewUi).isInstanceOf(TextContentPreviewUi::class.java) -        verify(transitionCallback, times(1)).onAllTransitionElementsReady() -    } - -    @Test -    fun test_filePreviewType_useFilePreviewUi() { -        whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE) -        val testSubject = -            ChooserContentPreviewUi( -                lifecycleOwner.lifecycle, -                previewData, -                Intent(Intent.ACTION_SEND), -                imageLoader, -                actionFactory, -                transitionCallback, -                headlineGenerator, -            ) -        assertThat(testSubject.preferredContentPreview) -            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.mContentPreviewUi).isInstanceOf(FileContentPreviewUi::class.java) -        verify(transitionCallback, times(1)).onAllTransitionElementsReady() -    } - -    @Test -    fun test_imagePreviewTypeWithText_useFilePlusTextPreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/img.png") -        whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        whenever(previewData.uriCount).thenReturn(2) -        whenever(previewData.firstFileInfo) -            .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) -        whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) -        val testSubject = -            ChooserContentPreviewUi( -                lifecycleOwner.lifecycle, -                previewData, -                Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") }, -                imageLoader, -                actionFactory, -                transitionCallback, -                headlineGenerator, -            ) -        assertThat(testSubject.mContentPreviewUi) -            .isInstanceOf(FilesPlusTextContentPreviewUi::class.java) -        verify(previewData, times(1)).imagePreviewFileInfoFlow -        verify(transitionCallback, times(1)).onAllTransitionElementsReady() -    } - -    @Test -    fun test_imagePreviewTypeWithoutText_useImagePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/img.png") -        whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        whenever(previewData.uriCount).thenReturn(2) -        whenever(previewData.firstFileInfo) -            .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) -        whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) -        val testSubject = -            ChooserContentPreviewUi( -                lifecycleOwner.lifecycle, -                previewData, -                Intent(Intent.ACTION_SEND), -                imageLoader, -                actionFactory, -                transitionCallback, -                headlineGenerator, -            ) -        assertThat(testSubject.preferredContentPreview) -            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java) -        verify(previewData, times(1)).imagePreviewFileInfoFlow -        verify(transitionCallback, never()).onAllTransitionElementsReady() -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt deleted file mode 100644 index 6db53a9e..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt +++ /dev/null @@ -1,41 +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.contentpreview - -import com.android.intentresolver.widget.ScrollableImagePreviewView.PreviewType -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class ContentPreviewUiTest { -    @Test -    fun testPreviewTypes() { -        val typeClassifier = -            object : MimeTypeClassifier { -                override fun isImageType(type: String?) = (type == "image") -                override fun isVideoType(type: String?) = (type == "video") -            } - -        assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "image")) -            .isEqualTo(PreviewType.Image) -        assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "video")) -            .isEqualTo(PreviewType.Video) -        assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "other")) -            .isEqualTo(PreviewType.File) -        assertThat(ContentPreviewUi.getPreviewType(typeClassifier, null)) -            .isEqualTo(PreviewType.File) -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt deleted file mode 100644 index fe13a215..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt +++ /dev/null @@ -1,225 +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.contentpreview - -import android.net.Uri -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.TextView -import androidx.lifecycle.testing.TestLifecycleOwner -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.android.intentresolver.R -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.android.intentresolver.widget.ActionRow -import com.google.common.truth.Truth.assertThat -import java.util.function.Consumer -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -private const val HEADLINE_IMAGES = "Image Headline" -private const val HEADLINE_VIDEOS = "Video Headline" -private const val HEADLINE_FILES = "Files Headline" -private const val SHARED_TEXT = "Some text to share" - -@RunWith(AndroidJUnit4::class) -class FilesPlusTextContentPreviewUiTest { -    private val lifecycleOwner = TestLifecycleOwner() -    private val actionFactory = -        object : ChooserContentPreviewUi.ActionFactory { -            override fun getEditButtonRunnable(): Runnable? = null -            override fun getCopyButtonRunnable(): Runnable? = null -            override fun createCustomActions(): List<ActionRow.Action> = emptyList() -            override fun getModifyShareAction(): ActionRow.Action? = null -            override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} -        } -    private val imageLoader = mock<ImageLoader>() -    private val headlineGenerator = -        mock<HeadlineGenerator> { -            whenever(getImagesHeadline(anyInt())).thenReturn(HEADLINE_IMAGES) -            whenever(getVideosHeadline(anyInt())).thenReturn(HEADLINE_VIDEOS) -            whenever(getFilesHeadline(anyInt())).thenReturn(HEADLINE_FILES) -        } - -    private val context -        get() = getInstrumentation().getContext() - -    @Test -    fun test_displayImagesPlusTextWithoutUriMetadata_showImagesHeadline() { -        val sharedFileCount = 2 -        val previewView = testLoadingHeadline("image/*", sharedFileCount) - -        verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_IMAGES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() { -        val sharedFileCount = 2 -        val previewView = testLoadingHeadline("video/*", sharedFileCount) - -        verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_VIDEOS) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayDocsPlusTextWithoutUriMetadata_showFilesHeadline() { -        val sharedFileCount = 2 -        val previewView = testLoadingHeadline("application/pdf", sharedFileCount) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_FILES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() { -        val sharedFileCount = 2 -        val previewView = testLoadingHeadline("*/*", sharedFileCount) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_FILES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayImagesPlusTextWithUriMetadataSet_showImagesHeadline() { -        val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg") -        val sharedFileCount = loadedFileMetadata.size -        val previewView = testLoadingHeadline("image/*", sharedFileCount, loadedFileMetadata) - -        verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_IMAGES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayVideosPlusTextWithUriMetadataSet_showVideosHeadline() { -        val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4") -        val sharedFileCount = loadedFileMetadata.size -        val previewView = testLoadingHeadline("video/*", sharedFileCount, loadedFileMetadata) - -        verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_VIDEOS) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayImagesAndVideosPlusTextWithUriMetadataSet_showFilesHeadline() { -        val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4") -        val sharedFileCount = loadedFileMetadata.size -        val previewView = testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_FILES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() { -        val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf") -        val sharedFileCount = loadedFileMetadata.size -        val previewView = -            testLoadingHeadline("application/pdf", sharedFileCount, loadedFileMetadata) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_FILES) -        verifySharedText(previewView) -    } - -    @Test -    fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() { -        val sharedFileCount = 2 -        val testSubject = -            FilesPlusTextContentPreviewUi( -                lifecycleOwner.lifecycle, -                /*isSingleImage=*/ false, -                sharedFileCount, -                SHARED_TEXT, -                /*intentMimeType=*/ "*/*", -                actionFactory, -                imageLoader, -                DefaultMimeTypeClassifier, -                headlineGenerator -            ) -        val layoutInflater = LayoutInflater.from(context) -        val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup - -        val previewView = -            testSubject.display(context.resources, LayoutInflater.from(context), gridLayout) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_FILES) - -        testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg")) - -        verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount) -        verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount) -        verifyPreviewHeadline(previewView, HEADLINE_IMAGES) -    } - -    private fun testLoadingHeadline( -        intentMimeType: String, -        sharedFileCount: Int, -        loadedFileMetadata: List<FileInfo>? = null -    ): ViewGroup? { -        val testSubject = -            FilesPlusTextContentPreviewUi( -                lifecycleOwner.lifecycle, -                /*isSingleImage=*/ false, -                sharedFileCount, -                SHARED_TEXT, -                intentMimeType, -                actionFactory, -                imageLoader, -                DefaultMimeTypeClassifier, -                headlineGenerator -            ) -        val layoutInflater = LayoutInflater.from(context) -        val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup - -        loadedFileMetadata?.let(testSubject::updatePreviewMetadata) -        return testSubject.display(context.resources, LayoutInflater.from(context), gridLayout) -    } - -    private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> { -        val uri = Uri.parse("content://pkg.app/file") -        return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() } -    } - -    private fun verifyPreviewHeadline(previewView: ViewGroup?, expectedText: String) { -        assertThat(previewView).isNotNull() -        val headlineView = previewView?.findViewById<TextView>(R.id.headline) -        assertThat(headlineView).isNotNull() -        assertThat(headlineView?.text).isEqualTo(expectedText) -    } - -    private fun verifySharedText(previewView: ViewGroup?) { -        assertThat(previewView).isNotNull() -        val textContentView = previewView?.findViewById<TextView>(R.id.content_preview_text) -        assertThat(textContentView).isNotNull() -        assertThat(textContentView?.text).isEqualTo(SHARED_TEXT) -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt deleted file mode 100644 index a65280e5..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt +++ /dev/null @@ -1,61 +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.contentpreview - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Test -import org.junit.runner.RunWith -import com.google.common.truth.Truth.assertThat - -@RunWith(AndroidJUnit4::class) -class HeadlineGeneratorImplTest { -    @Test -    fun testHeadlineGeneration() { -        val generator = HeadlineGeneratorImpl( -            InstrumentationRegistry.getInstrumentation().getTargetContext()) -        val str = "Some string" -        val url = "http://www.google.com" - -        assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") -        assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") - -        assertThat(generator.getImagesWithTextHeadline(str, 1)).isEqualTo("Sharing image with text") -        assertThat(generator.getImagesWithTextHeadline(url, 1)).isEqualTo("Sharing image with link") -        assertThat(generator.getImagesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 images with text") -        assertThat(generator.getImagesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 images with link") - -        assertThat(generator.getVideosWithTextHeadline(str, 1)).isEqualTo("Sharing video with text") -        assertThat(generator.getVideosWithTextHeadline(url, 1)).isEqualTo("Sharing video with link") -        assertThat(generator.getVideosWithTextHeadline(str, 5)).isEqualTo("Sharing 5 videos with text") -        assertThat(generator.getVideosWithTextHeadline(url, 5)).isEqualTo("Sharing 5 videos with link") - -        assertThat(generator.getFilesWithTextHeadline(str, 1)).isEqualTo("Sharing file with text") -        assertThat(generator.getFilesWithTextHeadline(url, 1)).isEqualTo("Sharing file with link") -        assertThat(generator.getFilesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 files with text") -        assertThat(generator.getFilesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 files with link") - -        assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") -        assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") - -        assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video") -        assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos") - -        assertThat(generator.getFilesHeadline(1)).isEqualTo("Sharing 1 file") -        assertThat(generator.getFilesHeadline(4)).isEqualTo("Sharing 4 files") -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt deleted file mode 100644 index b5fd1fa6..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt +++ /dev/null @@ -1,366 +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.contentpreview - -import android.content.ContentResolver -import android.graphics.Bitmap -import android.net.Uri -import android.util.Size -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope -import androidx.lifecycle.testing.TestLifecycleOwner -import com.android.intentresolver.any -import com.android.intentresolver.anyOrNull -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.google.common.truth.Truth.assertThat -import java.util.ArrayDeque -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit.MILLISECONDS -import java.util.concurrent.TimeUnit.SECONDS -import java.util.concurrent.atomic.AtomicInteger -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineStart.UNDISPATCHED -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Runnable -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import kotlinx.coroutines.yield -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -class ImagePreviewImageLoaderTest { -    private val imageSize = Size(300, 300) -    private val uriOne = Uri.parse("content://org.package.app/image-1.png") -    private val uriTwo = Uri.parse("content://org.package.app/image-2.png") -    private val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) -    private val contentResolver = -        mock<ContentResolver> { -            whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap) -        } -    private val lifecycleOwner = TestLifecycleOwner() -    private val dispatcher = UnconfinedTestDispatcher() -    private lateinit var testSubject: ImagePreviewImageLoader - -    @Before -    fun setup() { -        Dispatchers.setMain(dispatcher) -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) -        // create test subject after we've updated the lifecycle dispatcher -        testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -            ) -    } - -    @After -    fun cleanup() { -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) -        Dispatchers.resetMain() -    } - -    @Test -    fun prePopulate_cachesImagesUpToTheCacheSize() = runTest { -        testSubject.prePopulate(listOf(uriOne, uriTwo)) - -        verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) -        verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) - -        testSubject(uriOne) -        verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) -    } - -    @Test -    fun invoke_returnCachedImageWhenCalledTwice() = runTest { -        testSubject(uriOne) -        testSubject(uriOne) - -        verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) -    } - -    @Test -    fun invoke_whenInstructed_doesNotCache() = runTest { -        testSubject(uriOne, false) -        testSubject(uriOne, false) - -        verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) -    } - -    @Test -    fun invoke_overlappedRequests_Deduplicate() = runTest { -        val scheduler = TestCoroutineScheduler() -        val dispatcher = StandardTestDispatcher(scheduler) -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -            ) -        coroutineScope { -            launch(start = UNDISPATCHED) { testSubject(uriOne, false) } -            launch(start = UNDISPATCHED) { testSubject(uriOne, false) } -            scheduler.advanceUntilIdle() -        } - -        verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) -    } - -    @Test -    fun invoke_oldRecordsEvictedFromTheCache() = runTest { -        testSubject(uriOne) -        testSubject(uriTwo) -        testSubject(uriTwo) -        testSubject(uriOne) - -        verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) -        verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) -    } - -    @Test -    fun invoke_doNotCacheNulls() = runTest { -        whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) -        testSubject(uriOne) -        testSubject(uriOne) - -        verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) -    } - -    @Test(expected = CancellationException::class) -    fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest { -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) -        testSubject(uriOne) -    } - -    @Test(expected = CancellationException::class) -    fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest { -        val scheduler = TestCoroutineScheduler() -        val dispatcher = StandardTestDispatcher(scheduler) -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -            ) -        coroutineScope { -            val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } -            lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) -            scheduler.advanceUntilIdle() -            deferred.await() -        } -    } - -    @Test -    fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest { -        val scheduler = TestCoroutineScheduler() -        val dispatcher = StandardTestDispatcher(scheduler) -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -            ) -        coroutineScope { -            launch(start = UNDISPATCHED) { testSubject(uriOne, false) } -            launch(start = UNDISPATCHED) { testSubject(uriOne, true) } -            scheduler.advanceUntilIdle() -        } -        testSubject(uriOne, true) - -        verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) -    } - -    @Test -    fun invoke_semaphoreGuardsContentResolverCalls() = runTest { -        val contentResolver = -            mock<ContentResolver> { -                whenever(loadThumbnail(any(), any(), anyOrNull())) -                    .thenThrow(SecurityException("test")) -            } -        val acquireCount = AtomicInteger() -        val releaseCount = AtomicInteger() -        val testSemaphore = -            object : Semaphore { -                override val availablePermits: Int -                    get() = error("Unexpected invocation") - -                override suspend fun acquire() { -                    acquireCount.getAndIncrement() -                } - -                override fun tryAcquire(): Boolean { -                    error("Unexpected invocation") -                } - -                override fun release() { -                    releaseCount.getAndIncrement() -                } -            } - -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -                testSemaphore, -            ) -        testSubject(uriOne, false) - -        verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) -        assertThat(acquireCount.get()).isEqualTo(1) -        assertThat(releaseCount.get()).isEqualTo(1) -    } - -    @Test -    fun invoke_semaphoreIsReleasedAfterContentResolverFailure() = runTest { -        val semaphoreDeferred = CompletableDeferred<Unit>() -        val releaseCount = AtomicInteger() -        val testSemaphore = -            object : Semaphore { -                override val availablePermits: Int -                    get() = error("Unexpected invocation") - -                override suspend fun acquire() { -                    semaphoreDeferred.await() -                } - -                override fun tryAcquire(): Boolean { -                    error("Unexpected invocation") -                } - -                override fun release() { -                    releaseCount.getAndIncrement() -                } -            } - -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher, -                imageSize.width, -                contentResolver, -                cacheSize = 1, -                testSemaphore, -            ) -        launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - -        verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) - -        semaphoreDeferred.complete(Unit) - -        verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) -        assertThat(releaseCount.get()).isEqualTo(1) -    } - -    @Test -    fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() { -        val requestCount = 4 -        val thumbnailCallsCdl = CountDownLatch(requestCount) -        val pendingThumbnailCalls = ArrayDeque<CountDownLatch>() -        val contentResolver = -            mock<ContentResolver> { -                whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer { -                    val latch = CountDownLatch(1) -                    synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) } -                    thumbnailCallsCdl.countDown() -                    latch.await() -                    bitmap -                } -            } -        val name = "LoadImage" -        val maxSimultaneousRequests = 2 -        val threadsStartedCdl = CountDownLatch(requestCount) -        val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() } -        val testSubject = -            ImagePreviewImageLoader( -                lifecycleOwner.lifecycle.coroutineScope + dispatcher + CoroutineName(name), -                imageSize.width, -                contentResolver, -                cacheSize = 1, -                maxSimultaneousRequests, -            ) -        runTest { -            repeat(requestCount) { -                launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } -            } -            yield() -            // wait for all requests to be dispatched -            assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue() - -            assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() -            synchronized(pendingThumbnailCalls) { -                assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) -            } - -            pendingThumbnailCalls.poll()?.countDown() -            assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() -            synchronized(pendingThumbnailCalls) { -                assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) -            } - -            pendingThumbnailCalls.poll()?.countDown() -            assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue() -            synchronized(pendingThumbnailCalls) { -                assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) -            } -            for (cdl in pendingThumbnailCalls) { -                cdl.countDown() -            } -        } -    } -} - -private class NewThreadDispatcher( -    private val coroutineName: String, -    private val launchedCallback: () -> Unit -) : CoroutineDispatcher() { -    override fun isDispatchNeeded(context: CoroutineContext): Boolean = true - -    override fun dispatch(context: CoroutineContext, block: Runnable) { -        Thread { -                if (coroutineName == context[CoroutineName.Key]?.name) { -                    launchedCallback() -                } -                block.run() -            } -            .start() -    } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt deleted file mode 100644 index 6599baa9..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt +++ /dev/null @@ -1,349 +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.contentpreview - -import android.content.ContentInterface -import android.content.Intent -import android.database.MatrixCursor -import android.media.MediaMetadata -import android.net.Uri -import android.provider.DocumentsContract -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.google.common.truth.Truth.assertThat -import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.mockito.Mockito.any -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -class PreviewDataProviderTest { -    private val contentResolver = mock<ContentInterface>() -    private val mimeTypeClassifier = DefaultMimeTypeClassifier -    private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) - -    @Test -    fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() { -        val targetIntent = Intent(Intent.ACTION_VIEW) -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) -        verify(contentResolver, never()).getType(any()) -    } - -    @Test -    fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/notes.txt") -        val targetIntent = -            Intent(Intent.ACTION_SEND).apply { -                putExtra(Intent.EXTRA_STREAM, uri) -                type = "text/plain" -            } -        whenever(contentResolver.getType(uri)).thenReturn("text/plain") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() { -        val targetIntent = Intent(Intent.ACTION_SEND).apply { type = "image/png" } -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) -        verify(contentResolver, never()).getType(any()) -    } - -    @Test -    fun test_sendSingleImage_resolvesToImagePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/image.png") -        val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } -        whenever(contentResolver.getType(uri)).thenReturn("image/png") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri) -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_sendSingleNonImage_resolvesToFilePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/paper.pdf") -        val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } -        whenever(contentResolver.getType(uri)).thenReturn("application/pdf") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isNull() -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/image.png") -        val targetIntent = -            Intent(Intent.ACTION_SEND).apply { -                type = "image/png" -                putExtra(Intent.EXTRA_STREAM, uri) -            } -        whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isNull() -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/image.png") -        val targetIntent = -            Intent(Intent.ACTION_SEND).apply { -                type = "image/png" -                putExtra(Intent.EXTRA_STREAM, uri) -            } -        whenever(contentResolver.getStreamTypes(uri, "*/*")) -            .thenThrow(SecurityException("test failure")) -        whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) -            .thenThrow(SecurityException("test failure")) -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isNull() -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { -        val uri = Uri.parse("content://org.pkg.app/paper.pdf") -        val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } -        whenever(contentResolver.getStreamTypes(uri, "*/*")) -            .thenReturn(arrayOf("application/pdf", "image/png")) -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri) -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_SingleNonImageUriWithThumbnailFlag_useImagePreviewUi() { -        testMetadataToImagePreview( -            columns = arrayOf(DocumentsContract.Document.COLUMN_FLAGS), -            values = -                arrayOf( -                    DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL or -                        DocumentsContract.Document.FLAG_SUPPORTS_METADATA -                ) -        ) -    } - -    @Test -    fun test_SingleNonImageUriWithMetadataIconUri_useImagePreviewUi() { -        testMetadataToImagePreview( -            columns = arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI), -            values = arrayOf("content://org.pkg.app/test.pdf?thumbnail"), -        ) -    } - -    private fun testMetadataToImagePreview(columns: Array<String>, values: Array<Any>) { -        val uri = Uri.parse("content://org.pkg.app/test.pdf") -        val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } -        whenever(contentResolver.getType(uri)).thenReturn("application/pdf") -        whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) -            .thenReturn(MatrixCursor(columns).apply { addRow(values) }) -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(1) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) -        assertThat(testSubject.firstFileInfo?.previewUri).isNotNull() -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_multipleImageUri_useImagePreviewUi() { -        val uri1 = Uri.parse("content://org.pkg.app/test.png") -        val uri2 = Uri.parse("content://org.pkg.app/test.jpg") -        val targetIntent = -            Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                putExtra( -                    Intent.EXTRA_STREAM, -                    ArrayList<Uri>().apply { -                        add(uri1) -                        add(uri2) -                    } -                ) -            } -        whenever(contentResolver.getType(uri1)).thenReturn("image/png") -        whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(2) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) -        assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) -        // preview type can be determined by the first URI type -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_SomeImageUri_useImagePreviewUi() { -        val uri1 = Uri.parse("content://org.pkg.app/test.png") -        val uri2 = Uri.parse("content://org.pkg.app/test.pdf") -        whenever(contentResolver.getType(uri1)).thenReturn("image/png") -        whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") -        val targetIntent = -            Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                putExtra( -                    Intent.EXTRA_STREAM, -                    ArrayList<Uri>().apply { -                        add(uri1) -                        add(uri2) -                    } -                ) -            } -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(2) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) -        assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) -        // preview type can be determined by the first URI type -        verify(contentResolver, times(1)).getType(any()) -    } - -    @Test -    fun test_someNonImageUriWithPreview_useImagePreviewUi() { -        val uri1 = Uri.parse("content://org.pkg.app/test.mp4") -        val uri2 = Uri.parse("content://org.pkg.app/test.pdf") -        val targetIntent = -            Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                putExtra( -                    Intent.EXTRA_STREAM, -                    ArrayList<Uri>().apply { -                        add(uri1) -                        add(uri2) -                    } -                ) -            } -        whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4") -        whenever(contentResolver.getStreamTypes(uri1, "*/*")).thenReturn(arrayOf("image/png")) -        whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) -        assertThat(testSubject.uriCount).isEqualTo(2) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) -        assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) -        verify(contentResolver, times(2)).getType(any()) -    } - -    @Test -    fun test_allNonImageUrisWithoutPreview_useFilePreviewUi() { -        val uri1 = Uri.parse("content://org.pkg.app/test.html") -        val uri2 = Uri.parse("content://org.pkg.app/test.pdf") -        val targetIntent = -            Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                putExtra( -                    Intent.EXTRA_STREAM, -                    ArrayList<Uri>().apply { -                        add(uri1) -                        add(uri2) -                    } -                ) -            } -        whenever(contentResolver.getType(uri1)).thenReturn("text/html") -        whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") -        val testSubject = -            PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -        assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) -        assertThat(testSubject.uriCount).isEqualTo(2) -        assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) -        assertThat(testSubject.firstFileInfo?.previewUri).isNull() -        verify(contentResolver, times(2)).getType(any()) -    } - -    @Test -    fun test_imagePreviewFileInfoFlow_dataLoadedOnce() = -        testScope.runTest { -            val uri1 = Uri.parse("content://org.pkg.app/test.html") -            val uri2 = Uri.parse("content://org.pkg.app/test.pdf") -            val targetIntent = -                Intent(Intent.ACTION_SEND_MULTIPLE).apply { -                    putExtra( -                        Intent.EXTRA_STREAM, -                        ArrayList<Uri>().apply { -                            add(uri1) -                            add(uri2) -                        } -                    ) -                } -            whenever(contentResolver.getType(uri1)).thenReturn("text/html") -            whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") -            whenever(contentResolver.getStreamTypes(uri1, "*/*")) -                .thenReturn(arrayOf("text/html", "image/jpeg")) -            whenever(contentResolver.getStreamTypes(uri2, "*/*")) -                .thenReturn(arrayOf("application/pdf", "image/png")) -            val testSubject = -                PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier) - -            val fileInfoListOne = testSubject.imagePreviewFileInfoFlow.toList() -            val fileInfoListTwo = testSubject.imagePreviewFileInfoFlow.toList() - -            assertThat(fileInfoListOne).hasSize(2) -            assertThat(fileInfoListOne).containsAtLeastElementsIn(fileInfoListTwo).inOrder() - -            verify(contentResolver, times(1)).getType(uri1) -            verify(contentResolver, times(1)).getStreamTypes(uri1, "*/*") -            verify(contentResolver, times(1)).getType(uri2) -            verify(contentResolver, times(1)).getStreamTypes(uri2, "*/*") -        } -} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt deleted file mode 100644 index e7de0b7b..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt +++ /dev/null @@ -1,166 +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.contentpreview - -import android.net.Uri -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.android.intentresolver.R.layout.chooser_grid -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback -import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@RunWith(AndroidJUnit4::class) -class UnifiedContentPreviewUiTest { -    private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) -    private val actionFactory = -        mock<ChooserContentPreviewUi.ActionFactory> { -            whenever(createCustomActions()).thenReturn(emptyList()) -        } -    private val imageLoader = mock<ImageLoader>() -    private val headlineGenerator = -        mock<HeadlineGenerator> { -            whenever(getImagesHeadline(anyInt())).thenReturn("Image Headline") -            whenever(getVideosHeadline(anyInt())).thenReturn("Video Headline") -            whenever(getFilesHeadline(anyInt())).thenReturn("Files Headline") -        } - -    private val context -        get() = getInstrumentation().getContext() - -    @Test -    fun test_displayImagesWithoutUriMetadata_showImagesHeadline() { -        testLoadingHeadline("image/*", files = null) - -        verify(headlineGenerator, times(1)).getImagesHeadline(2) -    } - -    @Test -    fun test_displayVideosWithoutUriMetadata_showImagesHeadline() { -        testLoadingHeadline("video/*", files = null) - -        verify(headlineGenerator, times(1)).getVideosHeadline(2) -    } - -    @Test -    fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() { -        testLoadingHeadline("application/pdf", files = null) - -        verify(headlineGenerator, times(1)).getFilesHeadline(2) -    } - -    @Test -    fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() { -        testLoadingHeadline("*/*", files = null) - -        verify(headlineGenerator, times(1)).getFilesHeadline(2) -    } - -    @Test -    fun test_displayImagesWithUriMetadataSet_showImagesHeadline() { -        val uri = Uri.parse("content://pkg.app/image.png") -        val files = -            listOf( -                FileInfo.Builder(uri).withMimeType("image/png").build(), -                FileInfo.Builder(uri).withMimeType("image/jpeg").build(), -            ) -        testLoadingHeadline("image/*", files) - -        verify(headlineGenerator, times(1)).getImagesHeadline(2) -    } - -    @Test -    fun test_displayVideosWithUriMetadataSet_showImagesHeadline() { -        val uri = Uri.parse("content://pkg.app/image.png") -        val files = -            listOf( -                FileInfo.Builder(uri).withMimeType("video/mp4").build(), -                FileInfo.Builder(uri).withMimeType("video/mp4").build(), -            ) -        testLoadingHeadline("video/*", files) - -        verify(headlineGenerator, times(1)).getVideosHeadline(2) -    } - -    @Test -    fun test_displayImagesAndVideosWithUriMetadataSet_showImagesHeadline() { -        val uri = Uri.parse("content://pkg.app/image.png") -        val files = -            listOf( -                FileInfo.Builder(uri).withMimeType("image/png").build(), -                FileInfo.Builder(uri).withMimeType("video/mp4").build(), -            ) -        testLoadingHeadline("*/*", files) - -        verify(headlineGenerator, times(1)).getFilesHeadline(2) -    } - -    @Test -    fun test_displayDocumentsWithUriMetadataSet_showImagesHeadline() { -        val uri = Uri.parse("content://pkg.app/image.png") -        val files = -            listOf( -                FileInfo.Builder(uri).withMimeType("application/pdf").build(), -                FileInfo.Builder(uri).withMimeType("application/pdf").build(), -            ) -        testLoadingHeadline("application/pdf", files) - -        verify(headlineGenerator, times(1)).getFilesHeadline(2) -    } - -    private fun testLoadingHeadline(intentMimeType: String, files: List<FileInfo>?) { -        testScope.runTest { -            val endMarker = FileInfo.Builder(Uri.EMPTY).build() -            val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1) -            val testSubject = -                UnifiedContentPreviewUi( -                    testScope, -                    /*isSingleImage=*/ false, -                    intentMimeType, -                    actionFactory, -                    imageLoader, -                    DefaultMimeTypeClassifier, -                    object : TransitionElementStatusCallback { -                        override fun onTransitionElementReady(name: String) = Unit -                        override fun onAllTransitionElementsReady() = Unit -                    }, -                    files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker }, -                    /*itemCount=*/ 2, -                    headlineGenerator -                ) -            val layoutInflater = LayoutInflater.from(context) -            val gridLayout = layoutInflater.inflate(chooser_grid, null, false) as ViewGroup - -            testSubject.display(context.resources, LayoutInflater.from(context), gridLayout) -            emptySourceFlow.tryEmit(endMarker) -        } -    } -} diff --git a/java/tests/src/com/android/intentresolver/logging/EventLogTest.java b/java/tests/src/com/android/intentresolver/logging/EventLogTest.java deleted file mode 100644 index 17452774..00000000 --- a/java/tests/src/com/android/intentresolver/logging/EventLogTest.java +++ /dev/null @@ -1,422 +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.logging; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.AdditionalMatchers.gt; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import android.content.Intent; -import android.metrics.LogMaker; - -import com.android.intentresolver.logging.EventLog.FrameworkStatsLogger; -import com.android.intentresolver.logging.EventLog.SharesheetStandardEvent; -import com.android.intentresolver.logging.EventLog.SharesheetStartedEvent; -import com.android.intentresolver.logging.EventLog.SharesheetTargetSelectedEvent; -import com.android.intentresolver.contentpreview.ContentPreviewType; -import com.android.internal.logging.InstanceId; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLogger.UiEventEnum; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.FrameworkStatsLog; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public final class EventLogTest { -    @Mock private UiEventLogger mUiEventLog; -    @Mock private FrameworkStatsLogger mFrameworkLog; -    @Mock private MetricsLogger mMetricsLogger; - -    private EventLog mChooserLogger; - -    @Before -    public void setUp() { -        //Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger); -        mChooserLogger = new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger); -    } - -    @After -    public void tearDown() { -        verifyNoMoreInteractions(mUiEventLog); -        verifyNoMoreInteractions(mFrameworkLog); -        verifyNoMoreInteractions(mMetricsLogger); -    } - -    @Test -    public void testLogChooserActivityShown_personalProfile() { -        final boolean isWorkProfile = false; -        final String mimeType = "application/TestType"; -        final long systemCost = 456; - -        mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); - -        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN); -        assertThat(event.getSubtype()).isEqualTo(MetricsEvent.PARENT_PROFILE); -        assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType); -        assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS)) -                .isEqualTo(systemCost); -    } - -    @Test -    public void testLogChooserActivityShown_workProfile() { -        final boolean isWorkProfile = true; -        final String mimeType = "application/TestType"; -        final long systemCost = 456; - -        mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); - -        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN); -        assertThat(event.getSubtype()).isEqualTo(MetricsEvent.MANAGED_PROFILE); -        assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType); -        assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS)) -                .isEqualTo(systemCost); -    } - -    @Test -    public void testLogShareStarted() { -        final String packageName = "com.test.foo"; -        final String mimeType = "text/plain"; -        final int appProvidedDirectTargets = 123; -        final int appProvidedAppTargets = 456; -        final boolean workProfile = true; -        final int previewType = ContentPreviewType.CONTENT_PREVIEW_FILE; -        final String intentAction = Intent.ACTION_SENDTO; -        final int numCustomActions = 3; -        final boolean modifyShareProvided = true; - -        mChooserLogger.logShareStarted( -                packageName, -                mimeType, -                appProvidedDirectTargets, -                appProvidedAppTargets, -                workProfile, -                previewType, -                intentAction, -                numCustomActions, -                modifyShareProvided); - -        verify(mFrameworkLog).write( -                eq(FrameworkStatsLog.SHARESHEET_STARTED), -                eq(SharesheetStartedEvent.SHARE_STARTED.getId()), -                eq(packageName), -                /* instanceId=*/ gt(0), -                eq(mimeType), -                eq(appProvidedDirectTargets), -                eq(appProvidedAppTargets), -                eq(workProfile), -                eq(FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE), -                eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO), -                /* custom actions provided */ eq(numCustomActions), -                /* reselection action provided */ eq(modifyShareProvided)); -    } - -    @Test -    public void testLogShareTargetSelected() { -        final int targetType = EventLog.SELECTION_TYPE_SERVICE; -        final String packageName = "com.test.foo"; -        final int positionPicked = 123; -        final int directTargetAlsoRanked = -1; -        final int callerTargetCount = 0; -        final boolean isPinned = true; -        final boolean isSuccessfullySelected = true; -        final long selectionCost = 456; - -        mChooserLogger.logShareTargetSelected( -                targetType, -                packageName, -                positionPicked, -                directTargetAlsoRanked, -                callerTargetCount, -                /* directTargetHashed= */ null, -                isPinned, -                isSuccessfullySelected, -                selectionCost); - -        verify(mFrameworkLog).write( -                eq(FrameworkStatsLog.RANKING_SELECTED), -                eq(SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()), -                eq(packageName), -                /* instanceId=*/ gt(0), -                eq(positionPicked), -                eq(isPinned)); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); -        assertThat(event.getCategory()).isEqualTo( -                MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET); -        assertThat(event.getSubtype()).isEqualTo(positionPicked); -    } - -    @Test -    public void testLogActionSelected() { -        mChooserLogger.logActionSelected(EventLog.SELECTION_TYPE_COPY); - -        verify(mFrameworkLog).write( -                eq(FrameworkStatsLog.RANKING_SELECTED), -                eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()), -                eq(""), -                /* instanceId=*/ gt(0), -                eq(-1), -                eq(false)); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); -        assertThat(event.getCategory()).isEqualTo( -                MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET); -        assertThat(event.getSubtype()).isEqualTo(1); -    } - -    @Test -    public void testLogCustomActionSelected() { -        final int position = 4; -        mChooserLogger.logCustomActionSelected(position); - -        verify(mFrameworkLog).write( -                eq(FrameworkStatsLog.RANKING_SELECTED), -                eq(SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId()), -                any(), anyInt(), eq(position), eq(false)); -    } - -    @Test -    public void testLogDirectShareTargetReceived() { -        final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER; -        final int latency = 123; - -        mChooserLogger.logDirectShareTargetReceived(category, latency); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); -        assertThat(event.getCategory()).isEqualTo(category); -        assertThat(event.getSubtype()).isEqualTo(latency); -    } - -    @Test -    public void testLogActionShareWithPreview() { -        final int previewType = ContentPreviewType.CONTENT_PREVIEW_TEXT; - -        mChooserLogger.logActionShareWithPreview(previewType); - -        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); -        verify(mMetricsLogger).write(eventCaptor.capture()); -        LogMaker event = eventCaptor.getValue(); -        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_SHARE_WITH_PREVIEW); -        assertThat(event.getSubtype()).isEqualTo(previewType); -    } - -    @Test -    public void testLogSharesheetTriggered() { -        mChooserLogger.logSharesheetTriggered(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_TRIGGERED), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetAppLoadComplete() { -        mChooserLogger.logSharesheetAppLoadComplete(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetDirectLoadComplete() { -        mChooserLogger.logSharesheetDirectLoadComplete(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE), -                eq(0), -                isNull(), -                any()); -    } - -    @Test -    public void testLogSharesheetDirectLoadTimeout() { -        mChooserLogger.logSharesheetDirectLoadTimeout(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetProfileChanged() { -        mChooserLogger.logSharesheetProfileChanged(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetExpansionChanged_collapsed() { -        mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ true); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_COLLAPSED), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetExpansionChanged_expanded() { -        mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ false); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_EXPANDED), eq(0), isNull(), any()); -    } - -    @Test -    public void testLogSharesheetAppShareRankingTimeout() { -        mChooserLogger.logSharesheetAppShareRankingTimeout(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT), -                eq(0), -                isNull(), -                any()); -    } - -    @Test -    public void testLogSharesheetEmptyDirectShareRow() { -        mChooserLogger.logSharesheetEmptyDirectShareRow(); -        verify(mUiEventLog).logWithInstanceId( -                eq(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW), -                eq(0), -                isNull(), -                any()); -    } - -    @Test -    public void testDifferentLoggerInstancesUseDifferentInstanceIds() { -        ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class); -        EventLog chooserLogger2 = -                new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger); - -        final int targetType = EventLog.SELECTION_TYPE_COPY; -        final String packageName = "com.test.foo"; -        final int positionPicked = 123; -        final int directTargetAlsoRanked = -1; -        final int callerTargetCount = 0; -        final boolean isPinned = true; -        final boolean isSuccessfullySelected = true; -        final long selectionCost = 456; - -        mChooserLogger.logShareTargetSelected( -                targetType, -                packageName, -                positionPicked, -                directTargetAlsoRanked, -                callerTargetCount, -                /* directTargetHashed= */ null, -                isPinned, -                isSuccessfullySelected, -                selectionCost); - -        chooserLogger2.logShareTargetSelected( -                targetType, -                packageName, -                positionPicked, -                directTargetAlsoRanked, -                callerTargetCount, -                /* directTargetHashed= */ null, -                isPinned, -                isSuccessfullySelected, -                selectionCost); - -        verify(mFrameworkLog, times(2)).write( -                anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean()); - -        int id1 = idIntCaptor.getAllValues().get(0); -        int id2 = idIntCaptor.getAllValues().get(1); - -        assertThat(id1).isGreaterThan(0); -        assertThat(id2).isGreaterThan(0); -        assertThat(id1).isNotEqualTo(id2); -    } - -    @Test -    public void testUiAndFrameworkEventsUseSameInstanceIdForSameLoggerInstance() { -        ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class); -        ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class); - -        final int targetType = EventLog.SELECTION_TYPE_COPY; -        final String packageName = "com.test.foo"; -        final int positionPicked = 123; -        final int directTargetAlsoRanked = -1; -        final int callerTargetCount = 0; -        final boolean isPinned = true; -        final boolean isSuccessfullySelected = true; -        final long selectionCost = 456; - -        mChooserLogger.logShareTargetSelected( -                targetType, -                packageName, -                positionPicked, -                directTargetAlsoRanked, -                callerTargetCount, -                /* directTargetHashed= */ null, -                isPinned, -                isSuccessfullySelected, -                selectionCost); - -        verify(mFrameworkLog).write( -                anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean()); - -        mChooserLogger.logSharesheetTriggered(); -        verify(mUiEventLog).logWithInstanceId( -                any(UiEventEnum.class), anyInt(), any(), idObjectCaptor.capture()); - -        assertThat(idIntCaptor.getValue()).isGreaterThan(0); -        assertThat(idObjectCaptor.getValue().getId()).isEqualTo(idIntCaptor.getValue()); -    } - -    @Test -    public void testTargetSelectionCategories() { -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_SERVICE)) -                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET); -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_APP)) -                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET); -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_STANDARD)) -                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET); -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_COPY)).isEqualTo(0); -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_NEARBY)).isEqualTo(0); -        assertThat(EventLog.getTargetSelectionCategory( -                EventLog.SELECTION_TYPE_EDIT)).isEqualTo(0); -    } -} diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java deleted file mode 100644 index 5f0ead7b..00000000 --- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.model; - -import static junit.framework.Assert.assertEquals; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ResolveInfo; -import android.os.Message; - -import androidx.test.InstrumentationRegistry; - -import com.android.intentresolver.ResolvedComponentInfo; -import com.android.intentresolver.chooser.TargetInfo; - -import com.google.android.collect.Lists; - -import org.junit.Test; - -import java.util.List; - -public class AbstractResolverComparatorTest { - -    @Test -    public void testPinned() { -        ResolvedComponentInfo r1 = createResolvedComponentInfo( -                new ComponentName("package", "class")); -        r1.setPinned(true); - -        ResolvedComponentInfo r2 = createResolvedComponentInfo( -                new ComponentName("zackage", "zlass")); - -        Context context = InstrumentationRegistry.getTargetContext(); -        AbstractResolverComparator comparator = getTestComparator(context, null); - -        assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2)); -        assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1)); -    } - -    @Test -    public void testBothPinned() { -        ResolvedComponentInfo r1 = createResolvedComponentInfo( -                new ComponentName("package", "class")); -        r1.setPinned(true); - -        ResolvedComponentInfo r2 = createResolvedComponentInfo( -                new ComponentName("zackage", "zlass")); -        r2.setPinned(true); - -        Context context = InstrumentationRegistry.getTargetContext(); -        AbstractResolverComparator comparator = getTestComparator(context, null); - -        assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2)); -    } - -    @Test -    public void testPromoteToFirst() { -        ComponentName promoteToFirst = new ComponentName("promoted-package", "class"); -        ResolvedComponentInfo r1 = createResolvedComponentInfo(promoteToFirst); - -        ResolvedComponentInfo r2 = createResolvedComponentInfo( -                new ComponentName("package", "class")); - -        Context context = InstrumentationRegistry.getTargetContext(); -        AbstractResolverComparator comparator = getTestComparator(context, promoteToFirst); - -        assertEquals("PromoteToFirst ranks over non-cemented", -1, comparator.compare(r1, r2)); -        assertEquals("Non-cemented ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); -    } - -    @Test -    public void testPromoteToFirstOverPinned() { -        ComponentName cementedComponent = new ComponentName("promoted-package", "class"); -        ResolvedComponentInfo r1 = createResolvedComponentInfo(cementedComponent); - -        ResolvedComponentInfo r2 = createResolvedComponentInfo( -                new ComponentName("package", "class")); -        r2.setPinned(true); - -        Context context = InstrumentationRegistry.getTargetContext(); -        AbstractResolverComparator comparator = getTestComparator(context, cementedComponent); - -        assertEquals("PromoteToFirst ranks over pinned", -1, comparator.compare(r1, r2)); -        assertEquals("Pinned ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); -    } - -    private ResolvedComponentInfo createResolvedComponentInfo(ComponentName component) { -        ResolveInfo info = new ResolveInfo(); -        info.activityInfo = new ActivityInfo(); -        info.activityInfo.packageName = component.getPackageName(); -        info.activityInfo.name = component.getClassName(); -        return new ResolvedComponentInfo(component, new Intent(), info); -    } - -    private AbstractResolverComparator getTestComparator( -            Context context, ComponentName promoteToFirst) { -        Intent intent = new Intent(); - -        AbstractResolverComparator testComparator = -                new AbstractResolverComparator(context, intent, -                        Lists.newArrayList(context.getUser()), promoteToFirst) { - -                    @Override -                    int compare(ResolveInfo lhs, ResolveInfo rhs) { -                        // Used for testing pinning, so we should never get here --- the overrides -                        // should determine the result instead. -                        return 1; -                    } - -                    @Override -                    void doCompute(List<ResolvedComponentInfo> targets) {} - -                    @Override -                    public float getScore(TargetInfo targetInfo) { -                        return 0; -                    } - -                    @Override -                    void handleResultMessage(Message message) {} -                }; -        return testComparator; -    } - -} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt deleted file mode 100644 index 9b4a8057..00000000 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ /dev/null @@ -1,482 +0,0 @@ -/* - * 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.shortcuts - -import android.app.prediction.AppPredictor -import android.content.ComponentName -import android.content.Context -import android.content.IntentFilter -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ApplicationInfoFlags -import android.content.pm.ShortcutManager -import android.os.UserHandle -import android.os.UserManager -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.testing.TestLifecycleOwner -import androidx.test.filters.SmallTest -import com.android.intentresolver.any -import com.android.intentresolver.argumentCaptor -import com.android.intentresolver.capture -import com.android.intentresolver.chooser.DisplayResolveInfo -import com.android.intentresolver.createAppTarget -import com.android.intentresolver.createShareShortcutInfo -import com.android.intentresolver.createShortcutInfo -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import java.util.function.Consumer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -class ShortcutLoaderTest { -    private val appInfo = -        ApplicationInfo().apply { -            enabled = true -            flags = 0 -        } -    private val pm = -        mock<PackageManager> { -            whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) -        } -    private val userManager = -        mock<UserManager> { -            whenever(isUserRunning(any<UserHandle>())).thenReturn(true) -            whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true) -            whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false) -        } -    private val context = -        mock<Context> { -            whenever(packageManager).thenReturn(pm) -            whenever(createContextAsUser(any(), anyInt())).thenReturn(this) -            whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager) -        } -    private val scheduler = TestCoroutineScheduler() -    private val dispatcher = UnconfinedTestDispatcher(scheduler) -    private val lifecycleOwner = TestLifecycleOwner() -    private val intentFilter = mock<IntentFilter>() -    private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() -    private val callback = mock<Consumer<ShortcutLoader.Result>>() -    private val componentName = ComponentName("pkg", "Class") -    private val appTarget = -        mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) } -    private val appTargets = arrayOf(appTarget) -    private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) - -    @Before -    fun setup() { -        Dispatchers.setMain(dispatcher) -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) -    } - -    @After -    fun cleanup() { -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) -        Dispatchers.resetMain() -    } - -    @Test -    fun test_loadShortcutsWithAppPredictor_resultIntegrity() { -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                appPredictor, -                UserHandle.of(0), -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(appTargets) - -        val matchingAppTarget = createAppTarget(matchingShortcutInfo) -        val shortcuts = -            listOf( -                matchingAppTarget, -                // an AppTarget that does not belong to any resolved application; should be ignored -                createAppTarget( -                    createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) -                ) -            ) -        val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() -        verify(appPredictor, atLeastOnce()) -            .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) -        appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) - -        val resultCaptor = argumentCaptor<ShortcutLoader.Result>() -        verify(callback, times(1)).accept(capture(resultCaptor)) - -        val result = resultCaptor.value -        assertTrue("An app predictor result is expected", result.isFromAppPredictor) -        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) -        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) -        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) -        for (shortcut in result.shortcutsByApp[0].shortcuts) { -            assertEquals( -                "Wrong AppTarget in the cache", -                matchingAppTarget, -                result.directShareAppTargetCache[shortcut] -            ) -            assertEquals( -                "Wrong ShortcutInfo in the cache", -                matchingShortcutInfo, -                result.directShareShortcutInfoCache[shortcut] -            ) -        } -    } - -    @Test -    fun test_loadShortcutsWithShortcutManager_resultIntegrity() { -        val shortcutManagerResult = -            listOf( -                ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), -                // mismatching shortcut -                createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) -            ) -        val shortcutManager = -            mock<ShortcutManager> { -                whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) -            } -        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                null, -                UserHandle.of(0), -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(appTargets) - -        val resultCaptor = argumentCaptor<ShortcutLoader.Result>() -        verify(callback, times(1)).accept(capture(resultCaptor)) - -        val result = resultCaptor.value -        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) -        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) -        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) -        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) -        for (shortcut in result.shortcutsByApp[0].shortcuts) { -            assertTrue( -                "AppTargets are not expected the cache of a ShortcutManager result", -                result.directShareAppTargetCache.isEmpty() -            ) -            assertEquals( -                "Wrong ShortcutInfo in the cache", -                matchingShortcutInfo, -                result.directShareShortcutInfoCache[shortcut] -            ) -        } -    } - -    @Test -    fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() { -        val shortcutManagerResult = -            listOf( -                ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), -                // mismatching shortcut -                createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) -            ) -        val shortcutManager = -            mock<ShortcutManager> { -                whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) -            } -        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                appPredictor, -                UserHandle.of(0), -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(appTargets) - -        verify(appPredictor, times(1)).requestPredictionUpdate() -        val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() -        verify(appPredictor, times(1)) -            .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) -        appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) - -        val resultCaptor = argumentCaptor<ShortcutLoader.Result>() -        verify(callback, times(1)).accept(capture(resultCaptor)) - -        val result = resultCaptor.value -        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) -        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) -        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) -        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) -        for (shortcut in result.shortcutsByApp[0].shortcuts) { -            assertTrue( -                "AppTargets are not expected the cache of a ShortcutManager result", -                result.directShareAppTargetCache.isEmpty() -            ) -            assertEquals( -                "Wrong ShortcutInfo in the cache", -                matchingShortcutInfo, -                result.directShareShortcutInfoCache[shortcut] -            ) -        } -    } - -    @Test -    fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() { -        val shortcutManagerResult = -            listOf( -                ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), -                // mismatching shortcut -                createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) -            ) -        val shortcutManager = -            mock<ShortcutManager> { -                whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) -            } -        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) -        whenever(appPredictor.requestPredictionUpdate()) -            .thenThrow(IllegalStateException("Test exception")) -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                appPredictor, -                UserHandle.of(0), -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(appTargets) - -        verify(appPredictor, times(1)).requestPredictionUpdate() - -        val resultCaptor = argumentCaptor<ShortcutLoader.Result>() -        verify(callback, times(1)).accept(capture(resultCaptor)) - -        val result = resultCaptor.value -        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) -        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) -        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) -        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) -        for (shortcut in result.shortcutsByApp[0].shortcuts) { -            assertTrue( -                "AppTargets are not expected the cache of a ShortcutManager result", -                result.directShareAppTargetCache.isEmpty() -            ) -            assertEquals( -                "Wrong ShortcutInfo in the cache", -                matchingShortcutInfo, -                result.directShareShortcutInfoCache[shortcut] -            ) -        } -    } - -    @Test -    fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() { -        ShortcutLoader( -            context, -            lifecycleOwner.lifecycle, -            appPredictor, -            UserHandle.of(0), -            true, -            intentFilter, -            dispatcher, -            callback -        ) - -        verify(appPredictor, times(1)).requestPredictionUpdate() -        verify(callback, never()).accept(any()) -    } - -    @Test -    fun test_ShortcutLoader_noResultsWithoutAppTargets() { -        val shortcutManagerResult = -            listOf( -                ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), -                // mismatching shortcut -                createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) -            ) -        val shortcutManager = -            mock<ShortcutManager> { -                whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) -            } -        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                null, -                UserHandle.of(0), -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        verify(shortcutManager, times(1)).getShareTargets(any()) -        verify(callback, never()).accept(any()) - -        testSubject.reset() - -        verify(shortcutManager, times(2)).getShareTargets(any()) -        verify(callback, never()).accept(any()) - -        testSubject.updateAppTargets(appTargets) - -        verify(shortcutManager, times(2)).getShareTargets(any()) -        verify(callback, times(1)).accept(any()) -    } - -    @Test -    fun test_OnLifecycleDestroyed_unsubscribeFromAppPredictor() { -        ShortcutLoader( -            context, -            lifecycleOwner.lifecycle, -            appPredictor, -            UserHandle.of(0), -            true, -            intentFilter, -            dispatcher, -            callback -        ) - -        verify(appPredictor, never()).unregisterPredictionUpdates(any()) - -        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - -        verify(appPredictor, times(1)).unregisterPredictionUpdates(any()) -    } - -    @Test -    fun test_workProfileNotRunning_doNotCallServices() { -        testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) -    } - -    @Test -    fun test_workProfileLocked_doNotCallServices() { -        testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) -    } - -    @Test -    fun test_workProfileQuiteModeEnabled_doNotCallServices() { -        testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) -    } - -    @Test -    fun test_mainProfileNotRunning_callServicesAnyway() { -        testAlwaysCallSystemForMainProfile(isUserRunning = false) -    } - -    @Test -    fun test_mainProfileLocked_callServicesAnyway() { -        testAlwaysCallSystemForMainProfile(isUserUnlocked = false) -    } - -    @Test -    fun test_mainProfileQuiteModeEnabled_callServicesAnyway() { -        testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) -    } - -    private fun testDisabledWorkProfileDoNotCallSystem( -        isUserRunning: Boolean = true, -        isUserUnlocked: Boolean = true, -        isQuietModeEnabled: Boolean = false -    ) { -        val userHandle = UserHandle.of(10) -        with(userManager) { -            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) -            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) -            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) -        } -        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager) -        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() -        val callback = mock<Consumer<ShortcutLoader.Result>>() -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                appPredictor, -                userHandle, -                false, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) - -        verify(appPredictor, never()).requestPredictionUpdate() -    } - -    private fun testAlwaysCallSystemForMainProfile( -        isUserRunning: Boolean = true, -        isUserUnlocked: Boolean = true, -        isQuietModeEnabled: Boolean = false -    ) { -        val userHandle = UserHandle.of(10) -        with(userManager) { -            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) -            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) -            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) -        } -        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager) -        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() -        val callback = mock<Consumer<ShortcutLoader.Result>>() -        val testSubject = -            ShortcutLoader( -                context, -                lifecycleOwner.lifecycle, -                appPredictor, -                userHandle, -                true, -                intentFilter, -                dispatcher, -                callback -            ) - -        testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) - -        verify(appPredictor, times(1)).requestPredictionUpdate() -    } -} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt deleted file mode 100644 index e0de005d..00000000 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.shortcuts - -import android.app.prediction.AppTarget -import android.content.ComponentName -import android.content.Intent -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager.ShareShortcutInfo -import android.service.chooser.ChooserTarget -import com.android.intentresolver.createAppTarget -import com.android.intentresolver.createShareShortcutInfo -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test - -private const val PACKAGE = "org.package" - -class ShortcutToChooserTargetConverterTest { -    private val testSubject = ShortcutToChooserTargetConverter() -    private val ranks = arrayOf(3 ,7, 1 ,3) -    private val shortcuts = ranks -        .foldIndexed(ArrayList<ShareShortcutInfo>(ranks.size)) { i, acc, rank -> -            val id = i + 1 -            acc.add( -                createShareShortcutInfo( -                    id = "id-$i", -                    componentName = ComponentName(PACKAGE, "Class$id"), -                    rank, -                ) -            ) -            acc -        } - -    @Test -    fun testConvertToChooserTarget_predictionService() { -        val appTargets = shortcuts.map { createAppTarget(it.shortcutInfo) } -        val expectedOrderAllShortcuts = intArrayOf(0, 1, 2, 3) -        val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.98f, 0.97f) -        val appTargetCache = HashMap<ChooserTarget, AppTarget>() -        val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() - -        var chooserTargets = testSubject.convertToChooserTarget( -            shortcuts, -            shortcuts, -            appTargets, -            appTargetCache, -            shortcutInfoCache, -        ) - -        assertCorrectShortcutToChooserTargetConversion( -            shortcuts, -            chooserTargets, -            expectedOrderAllShortcuts, -            expectedScoreAllShortcuts, -        ) -        assertAppTargetCache(chooserTargets, appTargetCache) -        assertShortcutInfoCache(chooserTargets, shortcutInfoCache) - -        val subset = shortcuts.subList(1, shortcuts.size) -        val expectedOrderSubset = intArrayOf(1, 2, 3) -        val expectedScoreSubset = floatArrayOf(0.99f, 0.98f, 0.97f) -        appTargetCache.clear() -        shortcutInfoCache.clear() - -        chooserTargets = testSubject.convertToChooserTarget( -            subset, -            shortcuts, -            appTargets, -            appTargetCache, -            shortcutInfoCache, -        ) - -        assertCorrectShortcutToChooserTargetConversion( -            shortcuts, -            chooserTargets, -            expectedOrderSubset, -            expectedScoreSubset, -        ) -        assertAppTargetCache(chooserTargets, appTargetCache) -        assertShortcutInfoCache(chooserTargets, shortcutInfoCache) -    } - -    @Test -    fun testConvertToChooserTarget_shortcutManager() { -        val testSubject = ShortcutToChooserTargetConverter() -        val expectedOrderAllShortcuts = intArrayOf(2, 0, 3, 1) -        val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.99f, 0.98f) -        val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() - -        var chooserTargets = testSubject.convertToChooserTarget( -            shortcuts, -            shortcuts, -            null, -            null, -            shortcutInfoCache, -        ) - -        assertCorrectShortcutToChooserTargetConversion( -            shortcuts, chooserTargets, -            expectedOrderAllShortcuts, expectedScoreAllShortcuts -        ) -        assertShortcutInfoCache(chooserTargets, shortcutInfoCache) - -        val subset: MutableList<ShareShortcutInfo> = java.util.ArrayList() -        subset.add(shortcuts[1]) -        subset.add(shortcuts[2]) -        subset.add(shortcuts[3]) -        val expectedOrderSubset = intArrayOf(2, 3, 1) -        val expectedScoreSubset = floatArrayOf(1.0f, 0.99f, 0.98f) -        shortcutInfoCache.clear() - -        chooserTargets = testSubject.convertToChooserTarget( -            subset, -            shortcuts, -            null, -            null, -            shortcutInfoCache, -        ) - -        assertCorrectShortcutToChooserTargetConversion( -            shortcuts, chooserTargets, -            expectedOrderSubset, expectedScoreSubset -        ) -        assertShortcutInfoCache(chooserTargets, shortcutInfoCache) -    } - -    private fun assertCorrectShortcutToChooserTargetConversion( -        shortcuts: List<ShareShortcutInfo>, -        chooserTargets: List<ChooserTarget>, -        expectedOrder: IntArray, -        expectedScores: FloatArray, -    ) { -        assertEquals("Unexpected ChooserTarget count", expectedOrder.size, chooserTargets.size) -        for (i in chooserTargets.indices) { -            val ct = chooserTargets[i] -            val si = shortcuts[expectedOrder[i]].shortcutInfo -            val cn = shortcuts[expectedOrder[i]].targetComponent -            assertEquals(si.id, ct.intentExtras.getString(Intent.EXTRA_SHORTCUT_ID)) -            assertEquals(si.label, ct.title) -            assertEquals(expectedScores[i], ct.score) -            assertEquals(cn, ct.componentName) -        } -    } - -    private fun assertAppTargetCache( -        chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, AppTarget> -    ) { -        for (ct in chooserTargets) { -            val target = cache[ct] -            assertNotNull("AppTarget is missing", target) -        } -    } - -    private fun assertShortcutInfoCache( -        chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, ShortcutInfo> -    ) { -        for (ct in chooserTargets) { -            val si = cache[ct] -            assertNotNull("AppTarget is missing", si) -        } -    } -} diff --git a/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt b/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt deleted file mode 100644 index 18218064..00000000 --- a/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.android.intentresolver.util - -import android.app.PendingIntent -import android.content.IIntentReceiver -import android.content.IIntentSender -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.net.Uri -import android.os.Binder -import android.os.Bundle -import android.os.IBinder -import android.os.UserHandle -import android.service.chooser.ChooserAction -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class UriFiltersTest { - -    @Test -    fun uri_ownedByCurrentUser_noUserId() { -        val uri = Uri.parse("content://media/images/12345") -        assertTrue("Uri without userId should always return true", uri.ownedByCurrentUser) -    } - -    @Test -    fun uri_ownedByCurrentUser_selfUserId() { -        val uri = Uri.parse("content://${UserHandle.myUserId()}@media/images/12345") -        assertTrue("Uri with own userId should return true", uri.ownedByCurrentUser) -    } - -    @Test -    fun uri_ownedByCurrentUser_otherUserId() { -        val otherUserId = UserHandle.myUserId() + 10 -        val uri = Uri.parse("content://${otherUserId}@media/images/12345") -        assertFalse("Uri with other userId should return false", uri.ownedByCurrentUser) -    } - -    @Test -    fun chooserAction_hasValidIcon_bitmap() = -        smallBitmap().use { -            val icon = Icon.createWithBitmap(it) -            val action = actionWithIcon(icon) -            assertTrue("No uri, assumed valid", hasValidIcon(action)) -        } - -    @Test -    fun chooserAction_hasValidIcon_uri() { -        val icon = Icon.createWithContentUri("content://provider/content/12345") -        assertTrue("No userId in uri, uri is valid", hasValidIcon(actionWithIcon(icon))) -    } -    @Test -    fun chooserAction_hasValidIcon_uri_unowned() { -        val userId = UserHandle.myUserId() + 10 -        val icon = Icon.createWithContentUri("content://${userId}@provider/content/12345") -        assertFalse("uri userId references a different user", hasValidIcon(actionWithIcon(icon))) -    } - -    private fun smallBitmap() = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) - -    private fun mockAction(): PendingIntent { -        return PendingIntent( -            object : IIntentSender { -                override fun asBinder(): IBinder = Binder() -                override fun send( -                    code: Int, -                    intent: Intent?, -                    resolvedType: String?, -                    whitelistToken: IBinder?, -                    finishedReceiver: IIntentReceiver?, -                    requiredPermission: String?, -                    options: Bundle? -                ) { -                    /* empty */ -                } -            } -        ) -    } - -    private fun actionWithIcon(icon: Icon): ChooserAction { -        return ChooserAction.Builder(icon, "", mockAction()).build() -    } - -    /** Unconditionally recycles the [Bitmap] after running the given block */ -    private fun Bitmap.use(block: (Bitmap) -> Unit) = -        try { -            block(this) -        } finally { -            recycle() -        } -} diff --git a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt deleted file mode 100644 index 4f4223c0..00000000 --- a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt +++ /dev/null @@ -1,211 +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.widget - -import android.graphics.Bitmap -import android.net.Uri -import com.android.intentresolver.captureMany -import com.android.intentresolver.mock -import com.android.intentresolver.widget.ScrollableImagePreviewView.BatchPreviewLoader -import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview -import com.android.intentresolver.widget.ScrollableImagePreviewView.PreviewType -import com.android.intentresolver.withArgCaptor -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.atLeast -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -class BatchPreviewLoaderTest { -    private val dispatcher = UnconfinedTestDispatcher() -    private val testScope = CoroutineScope(dispatcher) -    private val onCompletion = mock<() -> Unit>() -    private val onUpdate = mock<(List<Preview>) -> Unit>() - -    @Before -    fun setup() { -        Dispatchers.setMain(dispatcher) -    } - -    @After -    fun cleanup() { -        testScope.cancel() -        Dispatchers.resetMain() -    } - -    @Test -    fun test_allImagesWithinViewPort_oneUpdate() { -        val imageLoader = TestImageLoader(testScope) -        val uriOne = createUri(1) -        val uriTwo = createUri(2) -        imageLoader.setUriLoadingOrder(succeed(uriTwo), succeed(uriOne)) -        val testSubject = -            BatchPreviewLoader( -                imageLoader, -                previews(uriOne, uriTwo), -                totalItemCount = 2, -                onUpdate, -                onCompletion -            ) -        testSubject.loadAspectRatios(200) { _, _, _ -> 100 } -        dispatcher.scheduler.advanceUntilIdle() - -        verify(onCompletion, times(1)).invoke() -        val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri } -        assertThat(list).containsExactly(uriOne, uriTwo).inOrder() -    } - -    @Test -    fun test_allImagesWithinViewPortOneFailed_failedPreviewIsNotUpdated() { -        val imageLoader = TestImageLoader(testScope) -        val uriOne = createUri(1) -        val uriTwo = createUri(2) -        val uriThree = createUri(3) -        imageLoader.setUriLoadingOrder(succeed(uriThree), fail(uriTwo), succeed(uriOne)) -        val testSubject = -            BatchPreviewLoader( -                imageLoader, -                previews(uriOne, uriTwo, uriThree), -                totalItemCount = 3, -                onUpdate, -                onCompletion -            ) -        testSubject.loadAspectRatios(200) { _, _, _ -> 100 } -        dispatcher.scheduler.advanceUntilIdle() - -        verify(onCompletion, times(1)).invoke() -        val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri } -        assertThat(list).containsExactly(uriOne, uriThree).inOrder() -    } - -    @Test -    fun test_imagesLoadedNotInOrder_updatedInOrder() { -        val imageLoader = TestImageLoader(testScope) -        val uris = Array(10) { createUri(it) } -        val loadingOrder = -            Array(uris.size) { i -> -                val uriIdx = -                    when { -                        i % 2 == 1 -> i - 1 -                        i % 2 == 0 && i < uris.size - 1 -> i + 1 -                        else -> i -                    } -                succeed(uris[uriIdx]) -            } -        imageLoader.setUriLoadingOrder(*loadingOrder) -        val testSubject = -            BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) -        testSubject.loadAspectRatios(200) { _, _, _ -> 100 } -        dispatcher.scheduler.advanceUntilIdle() - -        verify(onCompletion, times(1)).invoke() -        val list = -            captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) } -                .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } } -                .map { it.uri } -        assertThat(list).containsExactly(*uris).inOrder() -    } - -    @Test -    fun test_imagesLoadedNotInOrderSomeFailed_updatedInOrder() { -        val imageLoader = TestImageLoader(testScope) -        val uris = Array(10) { createUri(it) } -        val loadingOrder = -            Array(uris.size) { i -> -                val uriIdx = -                    when { -                        i % 2 == 1 -> i - 1 -                        i % 2 == 0 && i < uris.size - 1 -> i + 1 -                        else -> i -                    } -                if (uriIdx % 2 == 0) fail(uris[uriIdx]) else succeed(uris[uriIdx]) -            } -        val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) } -        imageLoader.setUriLoadingOrder(*loadingOrder) -        val testSubject = -            BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) -        testSubject.loadAspectRatios(200) { _, _, _ -> 100 } -        dispatcher.scheduler.advanceUntilIdle() - -        verify(onCompletion, times(1)).invoke() -        val list = -            captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) } -                .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } } -                .map { it.uri } -        assertThat(list).containsExactly(*expectedUris).inOrder() -    } - -    private fun createUri(idx: Int): Uri = Uri.parse("content://org.pkg.app/image-$idx.png") - -    private fun fail(uri: Uri) = uri to false -    private fun succeed(uri: Uri) = uri to true -    private fun previews(vararg uris: Uri) = -        uris -            .fold(ArrayList<Preview>(uris.size)) { acc, uri -> -                acc.apply { add(Preview(PreviewType.Image, uri, editAction = null)) } -            } -            .asFlow() -} - -private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? { -    private val loadingOrder = ArrayDeque<Pair<Uri, Boolean>>() -    private val pendingRequests = LinkedHashMap<Uri, CompletableDeferred<Bitmap?>>() -    private val flow = MutableSharedFlow<Unit>(replay = 1) -    private val bitmap by lazy { Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) } - -    init { -        scope.launch { -            flow.collect { -                while (true) { -                    val (nextUri, isLoaded) = loadingOrder.firstOrNull() ?: break -                    val deferred = pendingRequests.remove(nextUri) ?: break -                    loadingOrder.removeFirst() -                    deferred.complete(if (isLoaded) bitmap else null) -                } -                if (loadingOrder.isEmpty()) { -                    pendingRequests.forEach { (uri, deferred) -> deferred.complete(bitmap) } -                    pendingRequests.clear() -                } -            } -        } -    } - -    fun setUriLoadingOrder(vararg uris: Pair<Uri, Boolean>) { -        loadingOrder.clear() -        loadingOrder.addAll(uris) -    } - -    override suspend fun invoke(uri: Uri, cache: Boolean): Bitmap? { -        val deferred = pendingRequests.getOrPut(uri) { CompletableDeferred() } -        flow.tryEmit(Unit) -        return deferred.await() -    } -}  |