diff options
| author | 2024-01-17 22:14:31 -0800 | |
|---|---|---|
| committer | 2024-01-17 22:14:31 -0800 | |
| commit | efee97bcc526928fb7168072e0305f5a72324fbc (patch) | |
| tree | 7edfc23366f90cdca5852209a6ac207b7de884a4 /java | |
| parent | 3e303554182e65402022ecd079d63b94ce80ffe4 (diff) | |
| parent | 3007d9f481e92ed57ca9e3783719b3d84797ef2c (diff) | |
Merge Android 24Q1 Release (ab/11220357)
Bug: 319669529
Merged-In: I95e383e2822917198425acf9ba8bfbea76fdf948
Change-Id: Ibd7bfe1c21d32e1d0cc3023971afb779ed14c3a9
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() - } -} |