diff options
97 files changed, 1900 insertions, 1322 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index e8379205d55f..8b41aa4a7e7d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -8468,8 +8468,8 @@ public class Notification implements Parcelable } int maxAvatarSize = resources.getDimensionPixelSize( - isLowRam ? R.dimen.notification_person_icon_max_size - : R.dimen.notification_person_icon_max_size_low_ram); + isLowRam ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); if (mUser != null && mUser.getIcon() != null) { mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize); } diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java new file mode 100644 index 000000000000..65e8c646e17d --- /dev/null +++ b/core/java/com/android/internal/app/AppLocaleCollector.java @@ -0,0 +1,139 @@ +/* + * 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.internal.app; + +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; + +import android.content.Context; +import android.os.Build; +import android.os.LocaleList; +import android.util.Log; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** The Locale data collector for per-app language. */ +class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private static final String TAG = AppLocaleCollector.class.getSimpleName(); + private final Context mContext; + private final String mAppPackageName; + private final LocaleStore.LocaleInfo mAppCurrentLocale; + + AppLocaleCollector(Context context, String appPackageName) { + mContext = context; + mAppPackageName = appPackageName; + mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo( + mContext, mAppPackageName); + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> langTagsToIgnore = new HashSet<>(); + + LocaleList systemLangList = LocaleList.getDefault(); + for(int i = 0; i < systemLangList.size(); i++) { + langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); + } + + if (mAppCurrentLocale != null) { + langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag()); + } + return langTagsToIgnore; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + AppLocaleStore.AppLocaleResult result = + AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>(); + Set<LocaleStore.LocaleInfo> systemLocaleList; + boolean shouldShowList = + result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG + || result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_ASSET; + + // Get system supported locale list + if (isForCountryMode) { + systemLocaleList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + + // Add current app locale + if (mAppCurrentLocale != null && !isForCountryMode) { + appLocaleList.add(mAppCurrentLocale); + } + + // Add current system language into suggestion list + for(LocaleStore.LocaleInfo localeInfo: + LocaleStore.getSystemCurrentLocaleInfo()) { + boolean isNotCurrentLocale = mAppCurrentLocale == null + || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); + if (!isForCountryMode && isNotCurrentLocale) { + appLocaleList.add(localeInfo); + } + } + + // Add the languages that included in system supported locale + if (shouldShowList) { + appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale( + systemLocaleList, result.mAppSupportedLocales)); + } + + // Add "system language" option + if (!isForCountryMode && shouldShowList) { + appLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo( + mAppCurrentLocale == null)); + } + + if (Build.isDebuggable()) { + Log.d(TAG, "App locale list: " + appLocaleList); + } + + return appLocaleList; + } + + @Override + public boolean hasSpecificPackageName() { + return true; + } + + private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale( + Set<LocaleStore.LocaleInfo> systemLocaleList, + HashSet<Locale> appSupportedLocales) { + Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); + + for(LocaleStore.LocaleInfo li: systemLocaleList) { + if (appSupportedLocales.contains(li.getLocale())) { + filteredList.add(li); + } else { + for(Locale l: appSupportedLocales) { + if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { + filteredList.add(li); + break; + } + } + } + } + return filteredList; + } +} diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index 965895f08d6e..3efd279c2639 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -16,16 +16,12 @@ package com.android.internal.app; -import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus; - import android.app.FragmentManager; import android.app.FragmentTransaction; import android.app.ListFragment; import android.content.Context; import android.os.Bundle; -import android.os.LocaleList; import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -36,7 +32,6 @@ import android.widget.SearchView; import com.android.internal.R; -import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -54,6 +49,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private SuggestedLocaleAdapter mAdapter; private LocaleSelectedListener mListener; + private LocaleCollectorBase mLocalePickerCollector; private Set<LocaleStore.LocaleInfo> mLocaleList; private LocaleStore.LocaleInfo mParentLocale; private boolean mTranslatedOnly = false; @@ -62,7 +58,6 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private boolean mPreviousSearchHadFocus = false; private int mFirstVisiblePosition = 0; private int mTopDistance = 0; - private String mAppPackageName; private CharSequence mTitle = null; private OnActionExpandListener mOnActionExpandListener; @@ -79,31 +74,50 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O void onLocaleSelected(LocaleStore.LocaleInfo locale); } - private static LocalePickerWithRegion createCountryPicker(Context context, + /** + * The interface which provides the locale list. + */ + interface LocaleCollectorBase { + /** Gets the ignored locale list. */ + HashSet<String> getIgnoredLocaleList(boolean translatedOnly); + + /** Gets the supported locale list. */ + Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode); + + /** Indicates if the class work for specific package. */ + boolean hasSpecificPackageName(); + } + + private static LocalePickerWithRegion createCountryPicker( LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, - boolean translatedOnly, String appPackageName, - OnActionExpandListener onActionExpandListener) { + boolean translatedOnly, OnActionExpandListener onActionExpandListener, + LocaleCollectorBase localePickerCollector) { LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - boolean shouldShowTheList = localePicker.setListener(context, listener, parent, - translatedOnly, appPackageName); + boolean shouldShowTheList = localePicker.setListener(listener, parent, + translatedOnly, localePickerCollector); return shouldShowTheList ? localePicker : null; } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly) { - LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); - localePicker.setListener(context, listener, /* parent */ null, translatedOnly, null); - return localePicker; + return createLanguagePicker(context, listener, translatedOnly, null, null); } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly, String appPackageName, OnActionExpandListener onActionExpandListener) { + LocaleCollectorBase localePickerController; + if (TextUtils.isEmpty(appPackageName)) { + localePickerController = new SystemLocaleCollector(context); + } else { + localePickerController = new AppLocaleCollector(context, appPackageName); + } LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - localePicker.setListener( - context, listener, /* parent */ null, translatedOnly, appPackageName); + localePicker.setListener(listener, /* parent */ null, translatedOnly, + localePickerController); return localePicker; } @@ -120,109 +134,23 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O * In this case we don't even show the list, we call the listener with that locale, * "pretending" it was selected, and return false.</p> */ - private boolean setListener(Context context, LocaleSelectedListener listener, - LocaleStore.LocaleInfo parent, boolean translatedOnly, String appPackageName) { + private boolean setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, + boolean translatedOnly, LocaleCollectorBase localePickerController) { this.mParentLocale = parent; this.mListener = listener; this.mTranslatedOnly = translatedOnly; - this.mAppPackageName = appPackageName; + this.mLocalePickerCollector = localePickerController; setRetainInstance(true); - final HashSet<String> langTagsToIgnore = new HashSet<>(); - LocaleStore.LocaleInfo appCurrentLocale = - LocaleStore.getAppCurrentLocaleInfo(context, appPackageName); - boolean isForCountryMode = parent != null; - - if (!TextUtils.isEmpty(appPackageName) && !isForCountryMode) { - // Filter current system locale to add them into suggestion - LocaleList systemLangList = LocaleList.getDefault(); - for(int i = 0; i < systemLangList.size(); i++) { - langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); - } - - if (appCurrentLocale != null) { - Log.d(TAG, "appCurrentLocale: " + appCurrentLocale.getLocale().toLanguageTag()); - langTagsToIgnore.add(appCurrentLocale.getLocale().toLanguageTag()); - } else { - Log.d(TAG, "appCurrentLocale is null"); - } - } else if (!translatedOnly) { - final LocaleList userLocales = LocalePicker.getLocales(); - final String[] langTags = userLocales.toLanguageTags().split(","); - Collections.addAll(langTagsToIgnore, langTags); - } + mLocaleList = localePickerController.getSupportedLocaleList( + parent, translatedOnly, parent != null); - if (isForCountryMode) { - mLocaleList = LocaleStore.getLevelLocales(context, - langTagsToIgnore, parent, translatedOnly); - if (mLocaleList.size() <= 1) { - if (listener != null && (mLocaleList.size() == 1)) { - listener.onLocaleSelected(mLocaleList.iterator().next()); - } - return false; - } + if (parent != null && listener != null && mLocaleList.size() == 1) { + listener.onLocaleSelected(mLocaleList.iterator().next()); + return false; } else { - mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore, - null /* no parent */, translatedOnly); + return true; } - Log.d(TAG, "mLocaleList size: " + mLocaleList.size()); - - // Adding current locale and system default option into suggestion list - if(!TextUtils.isEmpty(appPackageName)) { - if (appCurrentLocale != null && !isForCountryMode) { - mLocaleList.add(appCurrentLocale); - } - - AppLocaleStore.AppLocaleResult result = - AppLocaleStore.getAppSupportedLocales(context, appPackageName); - boolean shouldShowList = - result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG - || result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; - - // Add current system language into suggestion list - for(LocaleStore.LocaleInfo localeInfo: LocaleStore.getSystemCurrentLocaleInfo()) { - boolean isNotCurrentLocale = appCurrentLocale == null - || !localeInfo.getLocale().equals(appCurrentLocale.getLocale()); - if (!isForCountryMode && isNotCurrentLocale) { - mLocaleList.add(localeInfo); - } - } - - // Filter the language not support in app - mLocaleList = filterTheLanguagesNotSupportedInApp( - shouldShowList, result.mAppSupportedLocales); - - Log.d(TAG, "mLocaleList after app-supported filter: " + mLocaleList.size()); - - // Add "system language" - if (!isForCountryMode && shouldShowList) { - mLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo(appCurrentLocale == null)); - } - } - return true; - } - - private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp( - boolean shouldShowList, HashSet<Locale> supportedLocales) { - Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); - if (!shouldShowList) { - return filteredList; - } - - for(LocaleStore.LocaleInfo li: mLocaleList) { - if (supportedLocales.contains(li.getLocale())) { - filteredList.add(li); - } else { - for(Locale l: supportedLocales) { - if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { - filteredList.add(li); - break; - } - } - } - } - - return filteredList; } private void returnToParentFrame() { @@ -246,7 +174,9 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O mTitle = getActivity().getTitle(); final boolean countryMode = mParentLocale != null; final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault(); - mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, mAppPackageName); + final boolean hasSpecificPackageName = + mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName(); + mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName); final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode); mAdapter.sort(comp); @@ -321,8 +251,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O returnToParentFrame(); } else { LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker( - getContext(), mListener, locale, mTranslatedOnly /* translate only */, - mAppPackageName, mOnActionExpandListener); + mListener, locale, mTranslatedOnly /* translate only */, + mOnActionExpandListener, this.mLocalePickerCollector); if (selector != null) { getFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) @@ -340,7 +270,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O inflater.inflate(R.menu.language_selection_list, menu); final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); - if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) { + if (mLocalePickerCollector.hasSpecificPackageName() + && mOnActionExpandListener != null) { searchMenuItem.setOnActionExpandListener(mOnActionExpandListener); } diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index 1be1247b7cc0..5ed0e8bc7883 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -70,17 +70,17 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { private Locale mDisplayLocale = null; // used to potentially cache a modified Context that uses mDisplayLocale private Context mContextOverride = null; - private String mAppPackageName; + private boolean mHasSpecificAppPackageName; public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) { - this(localeOptions, countryMode, null); + this(localeOptions, countryMode, false); } public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode, - String appPackageName) { + boolean hasSpecificAppPackageName) { mCountryMode = countryMode; mLocaleOptions = new ArrayList<>(localeOptions.size()); - mAppPackageName = appPackageName; + mHasSpecificAppPackageName = hasSpecificAppPackageName; for (LocaleStore.LocaleInfo li : localeOptions) { if (li.isSuggested()) { @@ -134,7 +134,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { @Override public int getViewTypeCount() { - if (!TextUtils.isEmpty(mAppPackageName) && showHeaders()) { + if (mHasSpecificAppPackageName && showHeaders()) { // Two headers, 1 "System language", 1 current locale return APP_LANGUAGE_PICKER_TYPE_COUNT; } else if (showHeaders()) { diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java new file mode 100644 index 000000000000..9a6d4c192fdc --- /dev/null +++ b/core/java/com/android/internal/app/SystemLocaleCollector.java @@ -0,0 +1,66 @@ +/* + * 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.internal.app; + +import android.content.Context; +import android.os.LocaleList; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** The Locale data collector for System language. */ +class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private final Context mContext; + + SystemLocaleCollector(Context context) { + mContext = context; + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> ignoreList = new HashSet<>(); + if (!translatedOnly) { + final LocaleList userLocales = LocalePicker.getLocales(); + final String[] langTags = userLocales.toLanguageTags().split(","); + Collections.addAll(ignoreList, langTags); + } + return ignoreList; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> localeList; + + if (isForCountryMode) { + localeList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + localeList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + return localeList; + } + + + @Override + public boolean hasSpecificPackageName() { + return false; + } +}
\ No newline at end of file diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index 2a3f916dcdec..e8e5a514d15f 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -1970,8 +1970,8 @@ <string name="usb_mtp_launch_notification_description" msgid="6942535713629852684">"Presiona para ver archivos"</string> <string name="pin_target" msgid="8036028973110156895">"Fijar"</string> <string name="pin_specific_target" msgid="7824671240625957415">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> - <string name="unpin_target" msgid="3963318576590204447">"No fijar"</string> - <string name="unpin_specific_target" msgid="3859828252160908146">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> + <string name="unpin_target" msgid="3963318576590204447">"Dejar de fijar"</string> + <string name="unpin_specific_target" msgid="3859828252160908146">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string> <string name="app_info" msgid="6113278084877079851">"Información de apps"</string> <string name="negative_duration" msgid="1938335096972945232">"−<xliff:g id="TIME">%1$s</xliff:g>"</string> <string name="demo_starting_message" msgid="6577581216125805905">"Iniciando demostración…"</string> diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml index 79faaac94919..0daa3249cc0c 100644 --- a/core/res/res/values-hi/strings.xml +++ b/core/res/res/values-hi/strings.xml @@ -1139,7 +1139,7 @@ <string name="copy" msgid="5472512047143665218">"कॉपी करें"</string> <string name="failed_to_copy_to_clipboard" msgid="725919885138539875">"क्लिपबोर्ड पर कॉपी नहीं हो सका"</string> <string name="paste" msgid="461843306215520225">"चिपकाएं"</string> - <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे पाठ के रूप में चिपकाएं"</string> + <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे टेक्स्ट के रूप में चिपकाएं"</string> <string name="replace" msgid="7842675434546657444">"बदलें•"</string> <string name="delete" msgid="1514113991712129054">"मिटाएं"</string> <string name="copyUrl" msgid="6229645005987260230">"यूआरएल को कॉपी करें"</string> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 9ab64e6f26df..d11eca61752f 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -252,7 +252,7 @@ <string name="bugreport_message" msgid="5212529146119624326">"현재 기기 상태에 대한 정보를 수집하여 이메일 메시지로 전송합니다. 버그 신고를 시작하여 전송할 준비가 되려면 약간 시간이 걸립니다."</string> <string name="bugreport_option_interactive_title" msgid="7968287837902871289">"대화형 보고서"</string> <string name="bugreport_option_interactive_summary" msgid="8493795476325339542">"대부분의 경우 이 옵션을 사용합니다. 신고 진행 상황을 추적하고 문제에 대한 세부정보를 입력하고 스크린샷을 찍을 수 있습니다. 신고하기에 시간이 너무 오래 걸리고 사용 빈도가 낮은 일부 섹션을 생략할 수 있습니다."</string> - <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 보고서"</string> + <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 신고"</string> <string name="bugreport_option_full_summary" msgid="1975130009258435885">"기기가 응답하지 않거나 너무 느리거나 모든 보고서 섹션이 필요한 경우 이 옵션을 사용하여 시스템 방해를 최소화합니다. 세부정보를 추가하거나 스크린샷을 추가로 찍을 수 없습니다."</string> <string name="bugreport_countdown" msgid="6418620521782120755">"{count,plural, =1{버그 신고 스크린샷을 #초 후에 찍습니다.}other{버그 신고 스크린샷을 #초 후에 찍습니다.}}"</string> <string name="bugreport_screenshot_success_toast" msgid="7986095104151473745">"버그 신고용 스크린샷 촬영 완료"</string> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0b8b29b9dda9..bcb13d2108b8 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -48,6 +48,7 @@ import static com.android.internal.util.ContrastColorUtilTest.assertContrastIsWi import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; @@ -56,7 +57,9 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.app.Notification.CallStyle; @@ -68,6 +71,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; @@ -79,7 +83,9 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; +import android.util.Pair; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -89,6 +95,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.util.ContrastColorUtil; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -218,8 +226,10 @@ public class NotificationTest { @Test public void allPendingIntents_recollectedAfterReusingBuilder() { - PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED); - PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE); Notification.Builder builder = new Notification.Builder(mContext, "channel"); builder.setContentIntent(intent1); @@ -669,30 +679,23 @@ public class NotificationTest { Notification notification = new Notification.Builder(mContext, "Channel").setStyle( style).build(); + int targetSize = mContext.getResources().getDimensionPixelSize( + ActivityManager.isLowRamDeviceStatic() + ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); + Bitmap personIcon = style.getUser().getIcon().getBitmap(); - assertThat(personIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(personIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(personIcon.getWidth()).isEqualTo(targetSize); + assertThat(personIcon.getHeight()).isEqualTo(targetSize); Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap(); - assertThat(avatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(avatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(avatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(avatarIcon.getHeight()).isEqualTo(targetSize); Bitmap historicAvatarIcon = style.getHistoricMessages().get( 0).getSenderPerson().getIcon().getBitmap(); - assertThat(historicAvatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(historicAvatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize); } @Test @@ -780,7 +783,6 @@ public class NotificationTest { assertFalse(notification.isMediaNotification()); } - @Test public void validateColorizedPaletteForColor(int rawColor) { Notification.Colors cDay = new Notification.Colors(); Notification.Colors cNight = new Notification.Colors(); @@ -861,19 +863,22 @@ public class NotificationTest { Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle()); - style.restoreFromExtras(fakeTypes); // no crash, good } @Test public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() { - Notification.Style style = new Notification.MessagingStyle(); + Notification.Style style = new Notification.MessagingStyle("test"); Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -885,22 +890,33 @@ public class NotificationTest { fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle()); fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @Test public void testRestoreFromExtras_Call_invalidExtra_noCrash() { - Notification.Style style = new CallStyle(); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Notification.Style style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("hi").build(), intent1, intent1); + Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); - + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -962,7 +978,11 @@ public class NotificationTest { fakeTypes.putParcelable(KEY_ON_READ, new Bundle()); fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle()); fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle()); - Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes); + + Notification n = new Notification.Builder(mContext, "test") + .setExtras(fakeTypes) + .build(); + Notification.CarExtender extender = new Notification.CarExtender(n); // no crash, good } @@ -980,6 +1000,493 @@ public class NotificationTest { // no crash, good } + + @Test + public void testDoesNotStripsExtenders() { + Notification.Builder nb = new Notification.Builder(mContext, "channel"); + nb.extend(new Notification.CarExtender().setColor(Color.RED)); + nb.extend(new Notification.TvExtender().setChannelId("different channel")); + nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); + Notification before = nb.build(); + Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); + + assertTrue(before == after); + + Assert.assertEquals("different channel", + new Notification.TvExtender(before).getChannelId()); + Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); + Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyles() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyleToStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_styleToNoStyle() { + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_changeStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle()); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testInboxTextChange() { + Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); + Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); + } + + @Test + public void testBigTextTextChange() { + Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("something")); + Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("else")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); + } + + @Test + public void testBigPictureChange() { + Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); + + Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); + Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); + } + + @Test + public void testMessagingChange_text() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build())) + .addMessage(new Notification.MessagingStyle.Message( + "b", 100, new Person.Builder().setName("hi").build())) + ); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_data() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()) + .setData("text", mock(Uri.class)))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_sender() { + Person a = new Person.Builder().setName("A").build(); + Person b = new Person.Builder().setName("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_key() { + Person a = new Person.Builder().setName("hi").setKey("A").build(); + Person b = new Person.Builder().setName("hi").setKey("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_ignoreTimeChange() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 1000, new Person.Builder().setName("hi").build())) + ); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testRemoteViews_nullChange() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test"); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(189); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(2); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(1); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testActionsDifferent_null() { + Notification n1 = new Notification.Builder(mContext, "test") + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSame() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentText() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSpannables() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, + new SpannableStringBuilder().append("test1", + new StyleSpan(Typeface.BOLD), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), + intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentNumber() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentIntent() { + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsIgnoresRemoteInputs() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"i", "m"}) + .build()) + .build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"t", "m"}) + .build()) + .build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testFreeformRemoteInputActionPair_noRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(false)); + } + + @Test + public void testFreeformRemoteInputActionPair_hasRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = new RemoteInput.Builder("a").build(); + + Notification.Action actionWithRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(remoteInput) + .build(); + + Notification.Action actionWithoutRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutRemoteInput) + .addAction(actionWithRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(false); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(remoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput( + new RemoteInput.Builder("a") + .setAllowFreeFormInput(false).build()) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(true)); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = + new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); + RemoteInput freeformRemoteInput = + new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); + + Notification.Action actionWithFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(freeformRemoteInput) + .build(); + + Notification.Action actionWithoutFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .addRemoteInput(remoteInput) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutFreeformRemoteInput) + .addAction(actionWithFreeformRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(true); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 0fdfed86b29e..17b1d1bdbff1 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string> <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string> <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string> + <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 351e1928a260..2babc3ea5fcd 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string> <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string> <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string> + <string name="handle_text" msgid="1766582106752184456">"مقبض"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 765aaba4d23c..fe43cd69110f 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"সৰ্বাধিক মাত্ৰালৈ বঢ়াওক"</string> <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string> <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string> + <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 0ec5db185e34..c22f5425048e 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Böyüdün"</string> <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string> <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string> + <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index bd4a079f54c6..cf5394b0ebd2 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string> <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index a04a50da7089..c525f521c0e3 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string> <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string> <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 2cc0ab321b4a..bec284d4dff8 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -84,6 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziranje"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string> <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string> - <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string> - <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string> + <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index a2badaf06989..c84bb4884068 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximitza"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string> <string name="close_button_text" msgid="2913281996024033299">"Tanca"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string> + <string name="handle_text" msgid="1766582106752184456">"Ansa"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 084ea865f769..60c1f8588445 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimér"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Luk"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 195c3355df2d..b57f0c857d26 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximieren"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string> <string name="close_button_text" msgid="2913281996024033299">"Schließen"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string> + <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 0ad387bbd39c..aed03acf026c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 4b85a22a12aa..bddc2c3e61bc 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index cc9057815ca2..696a520d0b86 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeeri"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string> <string name="close_button_text" msgid="2913281996024033299">"Sule"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string> + <string name="handle_text" msgid="1766582106752184456">"Käepide"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index a1d0b5851f84..7550a091bd79 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"بزرگ کردن"</string> <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string> <string name="close_button_text" msgid="2913281996024033299">"بستن"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string> + <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 5df1e04abb2a..a79adf8e686c 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Suurenna"</string> <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string> <string name="close_button_text" msgid="2913281996024033299">"Sulje"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string> + <string name="handle_text" msgid="1766582106752184456">"Kahva"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index c4b386aad3d1..865e2dcf4775 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string> <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> + <string name="handle_text" msgid="1766582106752184456">"Poignée"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index f5a106d3956d..47e6696dfe4c 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Pechar"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 95484d5fa1d5..2c643ded9dee 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string> <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string> + <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 5ae72eb27df2..d3bca3374e9d 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string> <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string> <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string> + <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index d36a83f115ec..f157fcf6dbec 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimalkan"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string> <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> + <string name="handle_text" msgid="1766582106752184456">"Tuas"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 31d483ed7c1a..ead1757ed4bb 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Stækka"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string> <string name="close_button_text" msgid="2913281996024033299">"Loka"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handfang"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 5c9425e92929..12c962d09ce1 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string> <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string> <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string> + <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 787ac3e1af00..b28436131fe9 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"მაქსიმალურად გაშლა"</string> <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string> <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"უკან"</string> + <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 927f0d7a0dde..2d842b88996a 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string> <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string> <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string> + <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 955b2f88f314..3243a648ac0e 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ពង្រីក"</string> <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string> <string name="close_button_text" msgid="2913281996024033299">"បិទ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string> + <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index a69e105876be..3a655b870df3 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string> <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string> <string name="close_button_text" msgid="2913281996024033299">"닫기"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string> + <string name="handle_text" msgid="1766582106752184456">"핸들"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 15bde88c7afb..b800e3eb174f 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string> <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string> <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string> + <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 275efa203a74..94339a46df6f 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Padidinti"</string> <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string> <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string> + <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 3048630e2344..d28245360922 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizēt"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string> <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string> + <string name="handle_text" msgid="1766582106752184456">"Turis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 9013828fb487..5db8d6d071f1 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"വലുതാക്കുക"</string> <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string> <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string> + <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index dc884e9bf3dd..779cf5c870bc 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"मोठे करा"</string> <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string> + <string name="handle_text" msgid="1766582106752184456">"हँडल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index a2f24f2c948b..0ceee2d1c928 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimer"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 6a70d8da05ed..7ba49e57111e 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string> <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string> + <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index d1d7db80caf5..dd3ebe415218 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maximaliseren"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string> <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> + <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index d2a96d95db74..c3056ca0a511 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string> <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string> <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string> + <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index cc2b2c314a85..56a6fb1664c1 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksymalizuj"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string> + <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 98a775e1e6b3..b96caf23a0a7 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Развернуть"</string> <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 57240618ef19..a1ec3b5d5bb2 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string> <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string> <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string> + <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 12f022e7a1f6..2f995e5076f3 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiraj"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zapri"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string> + <string name="handle_text" msgid="1766582106752184456">"Ročica"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index cdd870689886..3d9bde4f8066 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizo"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string> <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Pas"</string> + <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index fef3792b35a2..b39fd04ebf3d 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Utöka"</string> <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string> <string name="close_button_text" msgid="2913281996024033299">"Stäng"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handtag"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 2d10e2039a11..f4d4ceec5a1f 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Panua"</string> <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string> <string name="close_button_text" msgid="2913281996024033299">"Funga"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string> + <string name="handle_text" msgid="1766582106752184456">"Ncha"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 0eeeca78cb86..6d050c2ba26e 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string> <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string> <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string> + <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 2b105bdb7963..025e2e6215e7 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Ekranı Kapla"</string> <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string> <string name="close_button_text" msgid="2913281996024033299">"Kapat"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Geri"</string> + <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index a0925318ac26..97bb68080117 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string> <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 0330125c3e6f..ce73bd586c71 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Yoyish"</string> <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string> <string name="close_button_text" msgid="2913281996024033299">"Yopish"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 6e4a7682854d..511db6fbc116 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"Phóng to"</string> <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string> <string name="close_button_text" msgid="2913281996024033299">"Đóng"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string> + <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index df911ed55ab3..16cbf126eff8 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -84,8 +84,6 @@ <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"关闭"</string> - <!-- no translation found for back_button_text (1469718707134137085) --> - <skip /> - <!-- no translation found for handle_text (1766582106752184456) --> - <skip /> + <string name="back_button_text" msgid="1469718707134137085">"返回"</string> + <string name="handle_text" msgid="1766582106752184456">"处理"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index f48402264a73..59d87a0a9371 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -19,7 +19,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> - <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index e90389764af3..f209521b1da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -33,6 +33,8 @@ public class SplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -62,8 +64,12 @@ public class SplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } public SplitBounds(Parcel parcel) { @@ -75,6 +81,8 @@ public class SplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class SplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 7d1f130daaef..9d61c14e1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -101,9 +101,9 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final int shadowRadiusID = taskInfo.isFocused ? R.dimen.freeform_decor_shadow_focused_thickness : R.dimen.freeform_decor_shadow_unfocused_thickness; - final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -114,6 +114,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL int outsetRightId = R.dimen.freeform_resize_handle; int outsetBottomId = R.dimen.freeform_resize_handle; + mRelayoutParams.reset(); mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 01cab9aae312..b314163802ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -91,7 +91,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mTaskBackgroundSurface; SurfaceControl mCaptionContainerSurface; - private CaptionWindowManager mCaptionWindowManager; + private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private final Rect mCaptionInsetsRect = new Rect(); @@ -199,13 +199,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - final int decorContainerOffsetX = -loadResource(params.mOutsetLeftId); - final int decorContainerOffsetY = -loadResource(params.mOutsetTopId); + final Resources resources = mDecorWindowContext.getResources(); + final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() - + loadResource(params.mOutsetRightId) + + loadDimensionPixelSize(resources, params.mOutsetRightId) - decorContainerOffsetX; outResult.mHeight = taskBounds.height() - + loadResource(params.mOutsetBottomId) + + loadDimensionPixelSize(resources, params.mOutsetBottomId) - decorContainerOffsetY; startT.setPosition( mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) @@ -225,7 +226,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - float shadowRadius = loadResource(params.mShadowRadiusId); + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; @@ -248,8 +249,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = loadResource(params.mCaptionHeightId); - final int captionWidth = loadResource(params.mCaptionWidthId); + final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); //Prevent caption from going offscreen if task is too high up final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; @@ -264,8 +265,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new CaptionWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface); + mCaptionWindowManager = new WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); } // Caption view @@ -309,13 +311,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setCrop(mTaskSurface, mTaskSurfaceCrop); } - private int loadResource(int resourceId) { - if (resourceId == Resources.ID_NULL) { - return 0; - } - return mDecorWindowContext.getResources().getDimensionPixelSize(resourceId); - } - /** * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or * registers {@link #mOnDisplaysChangedListener} if it doesn't. @@ -374,33 +369,18 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } - static class RelayoutResult<T extends View & TaskFocusStateConsumer> { - int mWidth; - int mHeight; - T mRootView; - - void reset() { - mWidth = 0; - mHeight = 0; - mRootView = null; - } - } - - private static class CaptionWindowManager extends WindowlessWindowManager { - CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); + private static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; } + return resources.getDimensionPixelSize(resourceId); } - interface SurfaceControlViewHostFactory { - default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { - return new SurfaceControlViewHost(c, d, wmm); + private static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; } + return resources.getDimension(resourceId); } static class RelayoutParams{ @@ -433,6 +413,23 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetLeftId = Resources.ID_NULL; mOutsetRightId = Resources.ID_NULL; } + } + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { + int mWidth; + int mHeight; + T mRootView; + + void reset() { + mWidth = 0; + mHeight = 0; + mRootView = null; + } + } + + interface SurfaceControlViewHostFactory { + default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { + return new SurfaceControlViewHost(c, d, wmm); + } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..8949a75d1a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 103c8dab17d5..4d37e5dbc4dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -50,12 +50,13 @@ import android.view.WindowManager.LayoutParams; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; -import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.tests.R; import org.junit.Before; import org.junit.Test; @@ -145,8 +146,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -196,13 +200,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; -// int outsetLeftId = R.dimen.split_divider_bar_width; -// int outsetTopId = R.dimen.gestures_onehanded_drag_threshold; -// int outsetRightId = R.dimen.freeform_resize_handle; -// int outsetBottomId = R.dimen.bubble_dismiss_target_padding_x; -// mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -211,8 +213,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(decorContainerSurfaceBuilder).setParent(taskSurface); verify(decorContainerSurfaceBuilder).setContainerLayer(); verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); - verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -60, -60); - verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 420, 220); + verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40); + verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220); verify(taskBackgroundSurfaceBuilder).setParent(taskSurface); verify(taskBackgroundSurfaceBuilder).setEffectLayer(); @@ -225,36 +227,34 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -6, -156); - verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 432); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8); + verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); verify(mMockSurfaceControlViewHost) .setView(same(mMockView), - argThat(lp -> lp.height == 432 + argThat(lp -> lp.height == 64 && lp.width == 432 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); if (ViewRootImpl.CAPTION_ON_SHELL) { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 516), + new Rect(100, 300, 400, 332), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } verify(mMockSurfaceControlFinishT) .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y); verify(mMockSurfaceControlFinishT) - .setCrop(taskSurface, new Rect(-60, -60, 360, 160)); + .setCrop(taskSurface, new Rect(-20, -40, 360, 180)); verify(mMockSurfaceControlStartT) .show(taskSurface); - assertEquals(420, mRelayoutResult.mWidth); + assertEquals(380, mRelayoutResult.mWidth); assertEquals(220, mRelayoutResult.mHeight); - - } @Test @@ -293,8 +293,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -365,7 +368,8 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, testSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), @@ -417,12 +421,10 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - mRelayoutParams.mLayoutResId = 0; - mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_width; - mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; - mRelayoutParams.mShadowRadiusId = - R.dimen.freeform_decor_shadow_unfocused_thickness; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); diff --git a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml index 18b8ca9b4dd6..77dfbeee9bbf 100644 --- a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml +++ b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml @@ -16,7 +16,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="app_label" msgid="4470785958457506021">"隨附裝置管理員"</string> + <string name="app_label" msgid="4470785958457506021">"隨附裝置管理工具"</string> <string name="confirmation_title" msgid="3785000297483688997">"允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」<strong></strong>存取「<xliff:g id="DEVICE_NAME">%2$s</xliff:g>」<strong></strong>"</string> <string name="profile_name_watch" msgid="576290739483672360">"手錶"</string> <string name="chooser_title" msgid="2262294130493605839">"選擇要讓「<xliff:g id="APP_NAME">%2$s</xliff:g>」<strong></strong>管理的<xliff:g id="PROFILE_NAME">%1$s</xliff:g>"</string> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index f79b328190dd..5f1bb83715c2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -89,6 +89,11 @@ class TextAnimator( var y: Float = 0f /** + * The current line of text being drawn, in a multi-line TextView. + */ + var lineNo: Int = 0 + + /** * Mutable text size of the glyph in pixels. */ var textSize: Float = 0f diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index d427a57f3b87..0448c818f765 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -244,7 +244,7 @@ class TextInterpolator( canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) run.fontRuns.forEach { fontRun -> - drawFontRun(canvas, run, fontRun, tmpPaint) + drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) } } finally { canvas.restore() @@ -349,7 +349,7 @@ class TextInterpolator( var glyphFilter: GlyphCallback? = null // Draws single font run. - private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) { + private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) { var arrayIndex = 0 val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress) @@ -368,11 +368,13 @@ class TextInterpolator( tmpGlyph.font = font tmpGlyph.runStart = run.start tmpGlyph.runLength = run.end - run.start + tmpGlyph.lineNo = lineNo tmpPaintForGlyph.set(paint) var prevStart = run.start for (i in run.start until run.end) { + tmpGlyph.glyphIndex = i tmpGlyph.glyphId = line.glyphIds[i] tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress) tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress) diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 8457312dc403..cf66ff60f9ab 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -40,6 +40,10 @@ java_test_host { "tests/**/*.kt", "tests/**/*.java", ], + data: [ + ":framework", + ":androidx.annotation_annotation", + ], static_libs: [ "SystemUILintChecker", "junit", diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index 4eeeb850292a..4b9aa13c0240 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -32,7 +32,8 @@ import org.jetbrains.uast.UReferenceExpression class SoftwareBitmapDetector : Detector(), SourceCodeScanner { override fun getApplicableReferenceNames(): List<String> { - return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + return mutableListOf( + "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102") } override fun visitReference( @@ -40,13 +41,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { reference: UReferenceExpression, referenced: PsiElement ) { - val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( ISSUE, referenced, - context.getNameLocation(referenced), + context.getNameLocation(reference), "Replace software bitmap with `Config.HARDWARE`" ) } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt index d4c55c0d9149..141dd0535986 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -18,185 +18,22 @@ package com.android.internal.systemui.lint import com.android.annotations.NonNull import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile +import java.io.File import org.intellij.lang.annotations.Language @Suppress("UnstableApiUsage") @NonNull private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented() -internal val commonSettingsCode = - """ -public static float getFloat(ContentResolver cr, String name) { return 0.0f; } -public static long getLong(ContentResolver cr, String name) { - return 0L; -} -public static int getInt(ContentResolver cr, String name) { - return 0; -} -public static String getString(ContentResolver cr, String name) { - return ""; -} -public static float getFloat(ContentResolver cr, String name, float def) { - return 0.0f; -} -public static long getLong(ContentResolver cr, String name, long def) { - return 0L; -} -public static int getInt(ContentResolver cr, String name, int def) { - return 0; -} -public static String getString(ContentResolver cr, String name, String def) { - return ""; -} -public static boolean putFloat(ContentResolver cr, String name, float value) { - return true; -} -public static boolean putLong(ContentResolver cr, String name, long value) { - return true; -} -public static boolean putInt(ContentResolver cr, String name, int value) { - return true; -} -public static boolean putFloat(ContentResolver cr, String name) { - return true; -} -public static boolean putString(ContentResolver cr, String name, String value) { - return true; -} -""" - /* * This file contains stubs of framework APIs and System UI classes for testing purposes only. The * stubs are not used in the lint detectors themselves. */ internal val androidStubs = arrayOf( - indentedJava( - """ -package android.app; - -public class ActivityManager { - public static int getCurrentUser() {} -} -""" - ), - indentedJava( - """ -package android.accounts; - -public class AccountManager { - public static AccountManager get(Context context) { return null; } -} -""" - ), - indentedJava( - """ -package android.os; -import android.content.pm.UserInfo; -import android.annotation.UserIdInt; - -public class UserManager { - public UserInfo getUserInfo(@UserIdInt int userId) {} -} -""" - ), - indentedJava(""" -package android.annotation; - -public @interface UserIdInt {} -"""), - indentedJava(""" -package android.content.pm; - -public class UserInfo {} -"""), - indentedJava(""" -package android.os; - -public class Looper {} -"""), - indentedJava(""" -package android.os; - -public class Handler {} -"""), - indentedJava(""" -package android.content; - -public class ServiceConnection {} -"""), - indentedJava(""" -package android.os; - -public enum UserHandle { - ALL -} -"""), - indentedJava( - """ -package android.content; -import android.os.UserHandle; -import android.os.Handler; -import android.os.Looper; -import java.util.concurrent.Executor; - -public class Context { - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {} - public void registerReceiverAsUser( - BroadcastReceiver receiver, UserHandle user, IntentFilter filter, - String broadcastPermission, Handler scheduler) {} - public void registerReceiverForAllUsers( - BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, - Handler scheduler) {} - public void sendBroadcast(Intent intent) {} - public void sendBroadcast(Intent intent, String receiverPermission) {} - public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {} - public void bindService(Intent intent) {} - public void bindServiceAsUser( - Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {} - public void unbindService(ServiceConnection connection) {} - public Looper getMainLooper() { return null; } - public Executor getMainExecutor() { return null; } - public Handler getMainThreadHandler() { return null; } - public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; } - public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name); -} -""" - ), - indentedJava( - """ -package android.app; -import android.content.Context; - -public class Activity extends Context {} -""" - ), - indentedJava( - """ -package android.graphics; - -public class Bitmap { - public enum Config { - ARGB_8888, - RGB_565, - HARDWARE - } - public static Bitmap createBitmap(int width, int height, Config config) { - return null; - } -} -""" - ), - indentedJava(""" -package android.content; - -public class BroadcastReceiver {} -"""), - indentedJava(""" -package android.content; - -public class IntentFilter {} -"""), + LibraryReferenceTestFile(File("framework.jar").canonicalFile), + LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile), indentedJava( """ package com.android.systemui.settings; @@ -208,47 +45,4 @@ public interface UserTracker { } """ ), - indentedJava( - """ -package androidx.annotation; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.CONSTRUCTOR; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface WorkerThread { -} -""" - ), - indentedJava( - """ -package android.provider; - -public class Settings { - public static final class Global { - public static final String UNLOCK_SOUND = "unlock_sound"; - """ + - commonSettingsCode + - """ - } - public static final class Secure { - """ + - commonSettingsCode + - """ - } - public static final class System { - """ + - commonSettingsCode + - """ - } -} -""" - ), ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index 090ddf88fa3c..c632636eb9c8 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -51,12 +51,12 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { .run() .expect( """ - src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - ARGB_8888, - ~~~~~~~~~ - src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - RGB_565, - ~~~~~~~ + src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + ~~~~~~~ + src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + ~~~~~~~~~ 0 errors, 2 warnings """ ) @@ -67,7 +67,7 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { lint() .files( TestFiles.java( - """ + """ import android.graphics.Bitmap; public class TestClass { @@ -76,8 +76,7 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { } } """ - ) - .indented(), + ), *stubs ) .issues(SoftwareBitmapDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt index 2183b3805eed..3f93f075fe8b 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt @@ -3,9 +3,42 @@ package com.android.internal.systemui.lint import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestLintTask import java.io.File +import org.junit.ClassRule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement @Suppress("UnstableApiUsage") +@RunWith(JUnit4::class) abstract class SystemUILintDetectorTest : LintDetectorTest() { + + companion object { + @ClassRule + @JvmField + val libraryChecker: LibraryExists = + LibraryExists("framework.jar", "androidx.annotation_annotation.jar") + } + + class LibraryExists(vararg val libraryNames: String) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + for (libName in libraryNames) { + val libFile = File(libName) + if (!libFile.canonicalFile.exists()) { + throw Exception( + "Could not find $libName in the test's working directory. " + + "File ${libFile.absolutePath} does not exist." + ) + } + } + base.evaluate() + } + } + } + } /** * Customize the lint task to disable SDK usage completely. This ensures that running the tests * in Android Studio has the same result as running the tests in atest diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 1e74c3d68efc..dabb43b6074d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -14,6 +14,7 @@ package com.android.systemui.plugins import android.content.res.Resources +import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.View import com.android.systemui.plugins.annotations.ProvidesInterface @@ -114,6 +115,17 @@ interface ClockAnimations { /** Runs the battery animation (if any). */ fun charge() { } + + /** Move the clock, for example, if the notification tray appears in split-shade mode. */ + fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { } + + /** + * Whether this clock has a custom position update animation. If true, the keyguard will call + * `onPositionUpdated` to notify the clock of a position update animation. If false, a default + * animation will be used (e.g. a simple translation). + */ + val hasCustomPositionUpdatedAnimation + get() = false } /** Events that have specific data about the related face */ diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index 3ad7c8c4369c..d64587dcf362 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -37,6 +37,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" + android:clipChildren="false" android:visibility="gone" /> <!-- Not quite optimal but needed to translate these items as a group. The diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index bbe4b6c70c1c..f5fc7ee53f2e 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -672,7 +672,7 @@ <string name="data_connection_no_internet" msgid="691058178914184544">"沒有網際網路連線"</string> <string name="accessibility_quick_settings_open_settings" msgid="536838345505030893">"開啟「<xliff:g id="ID_1">%s</xliff:g>」設定。"</string> <string name="accessibility_quick_settings_edit" msgid="1523745183383815910">"編輯設定順序。"</string> - <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源按鈕選單"</string> + <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源鍵選單"</string> <string name="accessibility_quick_settings_page" msgid="7506322631645550961">"第 <xliff:g id="ID_1">%1$d</xliff:g> 頁,共 <xliff:g id="ID_2">%2$d</xliff:g> 頁"</string> <string name="tuner_lock_screen" msgid="2267383813241144544">"鎖定畫面"</string> <string name="thermal_shutdown_title" msgid="2702966892682930264">"手機先前過熱,因此關閉電源"</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 860a5da44088..1cf7c503a508 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -20,16 +20,15 @@ import android.annotation.ColorInt import android.annotation.FloatRange import android.annotation.IntRange import android.annotation.SuppressLint -import android.app.compat.ChangeIdStateCache.invalidate import android.content.Context import android.graphics.Canvas +import android.graphics.Rect import android.text.Layout import android.text.TextUtils import android.text.format.DateFormat import android.util.AttributeSet +import android.util.MathUtils import android.widget.TextView -import com.android.internal.R.attr.contentDescription -import com.android.internal.R.attr.format import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.GlyphCallback import com.android.systemui.animation.Interpolators @@ -39,6 +38,8 @@ import java.io.PrintWriter import java.util.Calendar import java.util.Locale import java.util.TimeZone +import kotlin.math.max +import kotlin.math.min /** * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30) @@ -189,8 +190,13 @@ class AnimatableClockView @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { lastDraw = getTimestamp() - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) + // Use textAnimator to render text if animation is enabled. + // Otherwise default to using standard draw functions. + if (isAnimationEnabled) { + textAnimator?.draw(canvas) + } else { + super.onDraw(canvas) + } } override fun invalidate() { @@ -311,7 +317,24 @@ class AnimatableClockView @JvmOverloads constructor( ) } - private val glyphFilter: GlyphCallback? = null // Add text animation tweak here. + // The offset of each glyph from where it should be. + private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + + private var lastSeenAnimationProgress = 1.0f + + // If the animation is being reversed, the target offset for each glyph for the "stop". + private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + private var animationCancelStopPosition = 0.0f + + // Whether the currently playing animation needed a stop (and thus, is shortened). + private var currentAnimationNeededStop = false + + private val glyphFilter: GlyphCallback = { positionedGlyph, _ -> + val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex + if (offset < glyphOffsets.size) { + positionedGlyph.x += glyphOffsets[offset] + } + } /** * Set text style with an optional animation. @@ -345,6 +368,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -359,6 +385,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } } } @@ -421,6 +450,124 @@ class AnimatableClockView @JvmOverloads constructor( pw.println(" time=$time") } + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + // Do we need to cancel an in-flight animation? + // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0, + // which trips up the check otherwise. + if (lastSeenAnimationProgress != 1.0f && + lastSeenAnimationProgress != 0.0f && + fraction == 0.0f) { + // New animation, but need to stop the old one. Figure out where each glyph currently + // is in relation to the box position. After that, use the leading digit's current + // position as the stop target. + currentAnimationNeededStop = true + + // We assume that the current glyph offsets would be relative to the "from" position. + val moveAmount = toRect.left - fromRect.left + + // Remap the current glyph offsets to be relative to the new "end" position, and figure + // out the start/end positions for the stop animation. + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + glyphOffsets[i] + animationCancelStartPosition[i] = glyphOffsets[i] + } + + // Use the leading digit's offset as the stop position. + if (toRect.left > fromRect.left) { + // It _was_ moving left + animationCancelStopPosition = glyphOffsets[0] + } else { + // It was moving right + animationCancelStopPosition = glyphOffsets[1] + } + } + + // Is there a cancellation in progress? + if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) { + val animationStopProgress = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction + ) + + // One of the digits has already stopped. + val animationStopStep = 1.0f / (NUM_DIGITS - 1) + + for (i in 0 until NUM_DIGITS) { + val stopAmount = if (toRect.left > fromRect.left) { + // It was moving left (before flipping) + MOVE_LEFT_DELAYS[i] * animationStopStep + } else { + // It was moving right (before flipping) + MOVE_RIGHT_DELAYS[i] * animationStopStep + } + + // Leading digit stops immediately. + if (stopAmount == 0.0f) { + glyphOffsets[i] = animationCancelStopPosition + } else { + val actualStopAmount = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress + ) + val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount) + val glyphMoveAmount = + animationCancelStopPosition - animationCancelStartPosition[i] + glyphOffsets[i] = + animationCancelStartPosition[i] + glyphMoveAmount * easedProgress + } + } + } else { + // Normal part of the animation. + // Do we need to remap the animation progress to take account of the cancellation? + val actualFraction = if (currentAnimationNeededStop) { + MathUtils.constrainedMap( + 0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction + ) + } else { + fraction + } + + val digitFractions = (0 until NUM_DIGITS).map { + // The delay for each digit, in terms of fraction (i.e. the digit should not move + // during 0.0 - 0.1). + val initialDelay = if (toRect.left > fromRect.left) { + MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP + } else { + MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP + } + + val f = MathUtils.constrainedMap( + 0.0f, 1.0f, + initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME, + actualFraction + ) + MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f)) + } + + // Was there an animation halt? + val moveAmount = if (currentAnimationNeededStop) { + // Only need to animate over the remaining space if the animation was aborted. + -animationCancelStopPosition + } else { + toRect.left.toFloat() - fromRect.left.toFloat() + } + + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i]) + } + } + + invalidate() + + if (fraction == 1.0f) { + // Reset + currentAnimationNeededStop = false + } + + lastSeenAnimationProgress = fraction + + // Ensure that the actual clock container is always in the "end" position. + this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom) + } + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private object Patterns { @@ -458,5 +605,36 @@ class AnimatableClockView @JvmOverloads constructor( private const val APPEAR_ANIM_DURATION: Long = 350 private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500 private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000 + + // Constants for the animation + private val MOVE_INTERPOLATOR = Interpolators.STANDARD + + // Calculate the positions of all of the digits... + // Offset each digit by, say, 0.1 + // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should + // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 + // from 0.3 - 1.0. + private const val NUM_DIGITS = 4 + private const val DIGITS_PER_LINE = 2 + + // How much of "fraction" to spend on canceling the animation, if needed + private const val ANIMATION_CANCELLATION_TIME = 0.4f + + // Delays. Each digit's animation should have a slight delay, so we get a nice + // "stepping" effect. When moving right, the second digit of the hour should move first. + // When moving left, the first digit of the hour should move first. The lists encode + // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied + // by delayMultiplier. + private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3) + private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2) + + // How much delay to apply to each subsequent digit. This is measured in terms of "fraction" + // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc + // before moving). + private const val MOVE_DIGIT_STEP = 0.1f + + // Total available transition time for each digit, taking into account the step. If step is + // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. + private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index e3c21cca2263..cd272635905b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -21,7 +21,6 @@ import android.os.Handler import android.os.UserHandle import android.provider.Settings import android.util.Log -import com.android.systemui.dagger.qualifiers.Main import com.android.internal.annotations.Keep import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId @@ -31,7 +30,6 @@ import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.PluginListener import com.android.systemui.shared.plugins.PluginManager import com.google.gson.Gson -import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName private const val DEBUG = true @@ -43,13 +41,6 @@ open class ClockRegistry( val handler: Handler, defaultClockProvider: ClockProvider ) { - @Inject constructor( - context: Context, - pluginManager: PluginManager, - @Main handler: Handler, - defaultClockProvider: DefaultClockProvider - ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } - // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { fun onClockChanged() diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index b88795157a43..6fd61daee6ff 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -16,6 +16,7 @@ package com.android.systemui.shared.clocks import android.content.Context import android.content.res.Resources import android.graphics.Color +import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater @@ -130,6 +131,10 @@ class DefaultClockController( lp.topMargin = (-0.5f * view.bottom).toInt() view.setLayoutParams(lp) } + + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + view.moveForSplitShade(fromRect, toRect, fraction) + } } inner class DefaultClockEvents : ClockEvents { @@ -209,6 +214,13 @@ class DefaultClockController( clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) } } } + + override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { + largeClock.moveForSplitShade(fromRect, toRect, fraction) + } + + override val hasCustomPositionUpdatedAnimation: Boolean + get() = true } private class AnimationState( diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index 72f8b7b09dca..40c8774d4f34 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -1,13 +1,16 @@ package com.android.systemui.shared.recents.utilities; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Surface; import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.wm.shell.util.SplitBounds; /** * Utility class to position the thumbnail in the TaskView @@ -16,10 +19,26 @@ public class PreviewPositionHelper { public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f; + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. private final RectF mClippedInsets = new RectF(); private final Matrix mMatrix = new Matrix(); private boolean mIsOrientationChanged; + private SplitBounds mSplitBounds; + private int mDesiredStagePosition; public Matrix getMatrix() { return mMatrix; @@ -33,6 +52,11 @@ public class PreviewPositionHelper { return mIsOrientationChanged; } + public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) { + mSplitBounds = splitBounds; + mDesiredStagePosition = desiredStagePosition; + } + /** * Updates the matrix based on the provided parameters */ @@ -42,10 +66,19 @@ public class PreviewPositionHelper { boolean isRotated = false; boolean isOrientationDifferent; + float fullscreenTaskWidth = screenWidthPx; + if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + } int thumbnailRotation = thumbnailData.rotation; int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float canvasScreenRatio = canvasWidth / (float) screenWidthPx; + float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; float scaledTaskbarSize = taskbarSize * canvasScreenRatio; thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; @@ -180,7 +213,7 @@ public class PreviewPositionHelper { * portrait or vice versa, {@code false} otherwise */ private boolean isOrientationChange(int deltaRotation) { - return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; + return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270; } private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) { @@ -189,13 +222,13 @@ public class PreviewPositionHelper { mMatrix.setRotate(90 * deltaRotate); switch (deltaRotate) { /* Counter-clockwise */ - case Surface.ROTATION_90: + case ROTATION_90: translateX = thumbnailPosition.height(); break; - case Surface.ROTATION_270: + case ROTATION_270: translateY = thumbnailPosition.width(); break; - case Surface.ROTATION_180: + case ROTATION_180: translateX = thumbnailPosition.width(); translateY = thumbnailPosition.height(); break; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 202134b699d7..35eecdf27f48 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -40,6 +40,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; @@ -404,5 +405,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS clock.dump(pw); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return getClock().getAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index e9f06eddf261..784974778af5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.util.Slog; import com.android.keyguard.KeyguardClockSwitch.ClockSize; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; @@ -232,4 +233,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mView.setClipBounds(null); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return mKeyguardClockSwitchController.getClockAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java index c4be1ba53503..72a44bd198f2 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java @@ -21,9 +21,14 @@ import java.util.List; import dagger.Module; import dagger.Provides; -/** Dagger Module for clock package. */ +/** + * Dagger Module for clock package. + * + * @deprecated Migrate to ClockRegistry + */ @Module -public abstract class ClockModule { +@Deprecated +public abstract class ClockInfoModule { /** */ @Provides diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java new file mode 100644 index 000000000000..f43f559b4234 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -0,0 +1,45 @@ +/* + * 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.keyguard.dagger; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.shared.clocks.ClockRegistry; +import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.plugins.PluginManager; + +import dagger.Module; +import dagger.Provides; + +/** Dagger Module for clocks. */ +@Module +public abstract class ClockRegistryModule { + /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */ + @Provides + @SysUISingleton + public static ClockRegistry getClockRegistry( + @Application Context context, + PluginManager pluginManager, + @Main Handler handler, + DefaultClockProvider defaultClockProvider) { + return new ClockRegistry(context, pluginManager, handler, defaultClockProvider); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 66a521c30f47..7d0109686351 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -21,13 +21,18 @@ import android.annotation.UiThread import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect -import android.hardware.biometrics.BiometricOverlayConstants +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.os.Build import android.os.RemoteException +import android.provider.Settings import android.util.Log import android.util.RotationUtils import android.view.LayoutInflater @@ -38,6 +43,7 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator @@ -54,13 +60,16 @@ import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" +@VisibleForTesting +const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" + /** * Keeps track of the overlay state and UI resources associated with a single FingerprintService * request. This state can persist across configuration changes via the [show] and [hide] * methods. */ @UiThread -class UdfpsControllerOverlay( +class UdfpsControllerOverlay @JvmOverloads constructor( private val context: Context, fingerprintManager: FingerprintManager, private val inflater: LayoutInflater, @@ -82,7 +91,8 @@ class UdfpsControllerOverlay( @ShowReason val requestReason: Int, private val controllerCallback: IUdfpsOverlayControllerCallback, private val onTouch: (View, MotionEvent, Boolean) -> Boolean, - private val activityLaunchAnimator: ActivityLaunchAnimator + private val activityLaunchAnimator: ActivityLaunchAnimator, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -102,18 +112,19 @@ class UdfpsControllerOverlay( gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or - WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY // Avoid announcing window title. accessibilityTitle = " " } /** A helper if the [requestReason] was due to enrollment. */ - val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) { - UdfpsEnrollHelper(context, fingerprintManager, requestReason) - } else { - null - } + val enrollHelper: UdfpsEnrollHelper? = + if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) { + UdfpsEnrollHelper(context, fingerprintManager, requestReason) + } else { + null + } /** If the overlay is currently showing. */ val isShowing: Boolean @@ -129,6 +140,17 @@ class UdfpsControllerOverlay( private var touchExplorationEnabled = false + private fun shouldRemoveEnrollmentUi(): Boolean { + if (isDebuggable) { + return Settings.Global.getInt( + context.contentResolver, + SETTING_REMOVE_ENROLLMENT_UI, + 0 /* def */ + ) != 0 + } + return false + } + /** Show the overlay or return false and do nothing if it is already showing. */ @SuppressLint("ClickableViewAccessibility") fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { @@ -183,7 +205,18 @@ class UdfpsControllerOverlay( view: UdfpsView, controller: UdfpsController ): UdfpsAnimationViewController<*>? { - return when (requestReason) { + val isEnrollment = when (requestReason) { + REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true + else -> false + } + + val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) { + REASON_AUTH_OTHER + } else { + requestReason + } + + return when (filteredRequestReason) { REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> { UdfpsEnrollViewController( @@ -198,7 +231,7 @@ class UdfpsControllerOverlay( overlayParams.scaleFactor ) } - BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> { + REASON_AUTH_KEYGUARD -> { UdfpsKeyguardViewController( view.addUdfpsView(R.layout.udfps_keyguard_view), statusBarStateController, @@ -216,7 +249,7 @@ class UdfpsControllerOverlay( activityLaunchAnimator ) } - BiometricOverlayConstants.REASON_AUTH_BP -> { + REASON_AUTH_BP -> { // note: empty controller, currently shows no visual affordance UdfpsBpViewController( view.addUdfpsView(R.layout.udfps_bp_view), @@ -226,8 +259,8 @@ class UdfpsControllerOverlay( dumpManager ) } - BiometricOverlayConstants.REASON_AUTH_OTHER, - BiometricOverlayConstants.REASON_AUTH_SETTINGS -> { + REASON_AUTH_OTHER, + REASON_AUTH_SETTINGS -> { UdfpsFpmOtherViewController( view.addUdfpsView(R.layout.udfps_fpm_other_view), statusBarStateController, @@ -440,4 +473,4 @@ private fun Int.isEnrollmentReason() = private fun Int.isImportantForAccessibility() = this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || - this == BiometricOverlayConstants.REASON_AUTH_BP + this == REASON_AUTH_BP diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index d7638d663dc9..7e31626983e7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -23,7 +23,8 @@ import android.service.dreams.IDreamManager; import androidx.annotation.Nullable; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.clock.ClockModule; +import com.android.keyguard.clock.ClockInfoModule; +import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -120,7 +121,8 @@ import dagger.Provides; BiometricsModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, - ClockModule.class, + ClockInfoModule.class, + ClockRegistryModule.class, CoroutinesModule.class, DreamModule.class, ControlsModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 00a9ddff1368..9415d6a90abc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -143,6 +143,12 @@ public class Flags { // TODO(b/254513102): Tracking Bug public static final ReleasedFlag USER_CONTROLLER_USES_INTERACTOR = new ReleasedFlag(211); + /** + * Whether the clock on a wide lock screen should use the new "stepping" animation for moving + * the digits when the clock moves. + */ + public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(212); + /***************************************/ // 300 - power menu // TODO(b/254512600): Tracking Bug @@ -278,7 +284,7 @@ public class Flags { public static final ReleasedFlag ROUNDED_BOX_RIPPLE = new ReleasedFlag(1002); // TODO(b/254512525): Tracking Bug - public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true); + public static final ReleasedFlag REFACTORED_DOCK_SETUP = new ReleasedFlag(1003); // 1100 - windowing @Keep diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c1afd59a1e22..6bba5a8b1386 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -79,7 +79,10 @@ import android.os.UserManager; import android.os.VibrationEffect; import android.provider.Settings; import android.transition.ChangeBounds; +import android.transition.Transition; import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.transition.TransitionValues; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; @@ -1440,6 +1443,16 @@ public final class NotificationPanelViewController { mMaxAllowedKeyguardNotifications = maxAllowed; } + @VisibleForTesting + boolean getClosing() { + return mClosing; + } + + @VisibleForTesting + boolean getIsFlinging() { + return mIsFlinging; + } + private void updateMaxDisplayedNotifications(boolean recompute) { if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); @@ -1667,9 +1680,40 @@ public final class NotificationPanelViewController { // horizontally properly. transition.excludeTarget(R.id.status_view_media_container, true); } + transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition); + + boolean customClockAnimation = + mKeyguardStatusViewController.getClockAnimations() != null + && mKeyguardStatusViewController.getClockAnimations() + .getHasCustomPositionUpdatedAnimation(); + + if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { + // Find the clock, so we can exclude it from this transition. + FrameLayout clockContainerView = + mView.findViewById(R.id.lockscreen_clock_view_large); + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); + + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardStatusViewController); + + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + adapter.addTarget(clockView); + set.addTransition(adapter); + + TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); + } else { + TransitionManager.beginDelayedTransition( + mNotificationContainerParent, transition); + } } constraintSet.applyTo(mNotificationContainerParent); @@ -3684,6 +3728,11 @@ public final class NotificationPanelViewController { setListening(true); } + @VisibleForTesting + void setTouchSlopExceeded(boolean isTouchSlopExceeded) { + mTouchSlopExceeded = isTouchSlopExceeded; + } + public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; @@ -4166,8 +4215,8 @@ public final class NotificationPanelViewController { /** * Sets the dozing state. * - * @param dozing {@code true} when dozing. - * @param animate if transition should be animated. + * @param dozing {@code true} when dozing. + * @param animate if transition should be animated. */ public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; @@ -4307,35 +4356,35 @@ public final class NotificationPanelViewController { /** * Starts fold to AOD animation. * - * @param startAction invoked when the animation starts. - * @param endAction invoked when the animation finishes, also if it was cancelled. + * @param startAction invoked when the animation starts. + * @param endAction invoked when the animation finishes, also if it was cancelled. * @param cancelAction invoked when the animation is cancelled, before endAction. */ public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { mView.animate() - .translationX(0) - .alpha(1f) - .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) - .setInterpolator(EMPHASIZED_DECELERATE) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - startAction.run(); - } + .translationX(0) + .alpha(1f) + .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) + .setInterpolator(EMPHASIZED_DECELERATE) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + startAction.run(); + } - @Override - public void onAnimationCancel(Animator animation) { - cancelAction.run(); - } + @Override + public void onAnimationCancel(Animator animation) { + cancelAction.run(); + } - @Override - public void onAnimationEnd(Animator animation) { - endAction.run(); - } - }).setUpdateListener(anim -> { - mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); - }).start(); + @Override + public void onAnimationEnd(Animator animation) { + endAction.run(); + } + }).setUpdateListener(anim -> { + mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); + }).start(); } /** @@ -4695,8 +4744,10 @@ public final class NotificationPanelViewController { /** * Maybe vibrate as panel is opened. * - * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead - * being opened programmatically (such as by the open panel gesture), we always play haptic. + * @param openingWithTouch Whether the panel is being opened with touch. If the panel is + * instead + * being opened programmatically (such as by the open panel gesture), we + * always play haptic. */ private void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening) { @@ -4742,6 +4793,7 @@ public final class NotificationPanelViewController { mAmbientState.setSwipingUp(false); if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop || Math.abs(y - mInitialExpandY) > mTouchSlop + || (!isFullyExpanded() && !isFullyCollapsed()) || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { mVelocityTracker.computeCurrentVelocity(1000); float vel = mVelocityTracker.getYVelocity(); @@ -4861,10 +4913,12 @@ public final class NotificationPanelViewController { animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; + @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } + @Override public void onAnimationEnd(Animator animation) { mIsSpringBackAnimation = false; @@ -4912,7 +4966,7 @@ public final class NotificationPanelViewController { if (isNaN(h)) { Log.wtf(TAG, "ExpandedHeight set to NaN"); } - mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { + mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> { if (mExpandLatencyTracking && h != 0f) { DejankUtils.postAfterTraversal( () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); @@ -5103,7 +5157,7 @@ public final class NotificationPanelViewController { /** * Create an animator that can also overshoot * - * @param targetHeight the target height + * @param targetHeight the target height * @param overshootAmount the amount of overshoot desired */ private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { @@ -5900,7 +5954,7 @@ public final class NotificationPanelViewController { public final class TouchHandler implements View.OnTouchListener { private long mLastTouchDownTime = -1L; - /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ + /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ public boolean onInterceptTouchEvent(MotionEvent event) { if (SPEW_LOGCAT) { Log.v(TAG, @@ -6099,7 +6153,7 @@ public final class NotificationPanelViewController { mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); return false; } - if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); return false; } @@ -6256,4 +6310,54 @@ public final class NotificationPanelViewController { loadDimens(); } } + + static class SplitShadeTransitionAdapter extends Transition { + private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; + private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; + + private final KeyguardStatusViewController mController; + + SplitShadeTransitionAdapter(KeyguardStatusViewController controller) { + mController = controller; + } + + private void captureValues(TransitionValues transitionValues) { + Rect boundsRect = new Rect(); + boundsRect.left = transitionValues.view.getLeft(); + boundsRect.top = transitionValues.view.getTop(); + boundsRect.right = transitionValues.view.getRight(); + boundsRect.bottom = transitionValues.view.getBottom(); + transitionValues.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + + Rect from = (Rect) startValues.values.get(PROP_BOUNDS); + Rect to = (Rect) endValues.values.get(PROP_BOUNDS); + + anim.addUpdateListener( + animation -> mController.getClockAnimations().onPositionUpdated( + from, to, animation.getAnimatedFraction())); + + return anim; + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8f3eb4f7e223..8a31ed9271ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap +import android.util.ArraySet +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -70,6 +72,7 @@ class HeadsUpCoordinator @Inject constructor( @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() + private val mEntriesUpdateTimes = ArrayMap<String, Long>() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 @@ -264,6 +267,9 @@ class HeadsUpCoordinator @Inject constructor( } // After this method runs, all posted entries should have been handled (or skipped). mPostedEntries.clear() + + // Also take this opportunity to clean up any stale entry update times + cleanUpEntryUpdateTimes() } /** @@ -378,6 +384,9 @@ class HeadsUpCoordinator @Inject constructor( isAlerting = false, isBinding = false, ) + + // Record the last updated time for this key + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -419,6 +428,9 @@ class HeadsUpCoordinator @Inject constructor( cancelHeadsUpBind(posted.entry) } } + + // Update last updated time for this entry + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -426,6 +438,7 @@ class HeadsUpCoordinator @Inject constructor( */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { mPostedEntries.remove(entry.key) + mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { @@ -454,7 +467,12 @@ class HeadsUpCoordinator @Inject constructor( // never) in mPostedEntries to need to alert, we need to check every notification // known to the pipeline. for (entry in mNotifPipeline.allNotifs) { - // The only entries we can consider alerting for here are entries that have never + // Only consider entries that are recent enough, since we want to apply a fairly + // strict threshold for when an entry should be updated via only ranking and not an + // app-provided notification update. + if (!isNewEnoughForRankingUpdate(entry)) continue + + // The only entries we consider alerting for here are entries that have never // interrupted and that now say they should heads up; if they've alerted in the // past, we don't want to incorrectly alert a second time if there wasn't an // explicit notification update. @@ -486,6 +504,41 @@ class HeadsUpCoordinator @Inject constructor( (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) } + /** + * Sets the updated time for the given entry to the specified time. + */ + @VisibleForTesting + fun setUpdateTime(entry: NotificationEntry, time: Long) { + mEntriesUpdateTimes[entry.key] = time + } + + /** + * Checks whether the entry is new enough to be updated via ranking update. + * We want to avoid updating an entry too long after it was originally posted/updated when we're + * only reacting to a ranking change, as relevant ranking updates are expected to come in + * fairly soon after the posting of a notification. + */ + private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean { + // If we don't have an update time for this key, default to "too old" + if (!mEntriesUpdateTimes.containsKey(entry.key)) return false + + val updateTime = mEntriesUpdateTimes[entry.key] ?: return false + return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS + } + + private fun cleanUpEntryUpdateTimes() { + // Because we won't update entries that are older than this amount of time anyway, clean + // up any entries that are too old to notify. + val toRemove = ArraySet<String>() + for ((key, updateTime) in mEntriesUpdateTimes) { + if (updateTime == null || + (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) { + toRemove.add(key) + } + } + mEntriesUpdateTimes.removeAll(toRemove) + } + /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> if (mNotifsExtendingLifetime.contains(entry)) { @@ -597,6 +650,9 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" private const val BIND_TIMEOUT = 1000L + + // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord. + private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000 } data class PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt index 7f1195b78c77..7da2d47c1226 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -17,7 +17,8 @@ package com.android.systemui.user import android.os.Bundle -import android.view.View +import android.view.WindowInsets.Type +import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE import androidx.activity.ComponentActivity import androidx.lifecycle.ViewModelProvider import com.android.systemui.R @@ -38,10 +39,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.user_switcher_fullscreen) - window.decorView.systemUiVisibility = - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + window.decorView.getWindowInsetsController().apply { + setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) + hide(Type.systemBars()) + } val viewModel = ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] UserSwitcherViewBinder.bind( diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index bb03a47e025c..627d738a895f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Rect; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; @@ -45,6 +46,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; @@ -262,6 +264,19 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true); } + @Test + public void testGetClockAnimationsForwardsToClock() { + ClockController mockClockController = mock(ClockController.class); + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mClockEventController.getClock()).thenReturn(mockClockController); + when(mockClockController.getAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f); + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f); + } + private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 4dcaa7cf8c09..c94c97c9b638 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -16,12 +16,16 @@ package com.android.keyguard; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.graphics.Rect; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -108,4 +112,16 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onLocaleListChanged(); verify(mKeyguardClockSwitchController).onLocaleListChanged(); } + + @Test + public void getClockAnimations_forwardsToClockSwitch() { + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mKeyguardClockSwitchController.getClockAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.3f); + + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.3f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index baeabc577fb7..c85334db9499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_ import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater @@ -124,14 +125,18 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { whenever(udfpsEnrollView.context).thenReturn(context) } - private fun withReason(@ShowReason reason: Int, block: () -> Unit) { + private fun withReason( + @ShowReason reason: Int, + isDebuggable: Boolean = false, + block: () -> Unit + ) { controllerOverlay = UdfpsControllerOverlay( context, fingerprintManager, inflater, windowManager, accessibilityManager, statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, configurationController, systemClock, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, - controllerCallback, onTouch, activityLaunchAnimator + controllerCallback, onTouch, activityLaunchAnimator, isDebuggable ) block() } @@ -151,11 +156,29 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { } @Test + fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { showUdfpsOverlay(isEnrollUseCase = true) } @Test + fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() } private fun withRotation(@Rotation rotation: Int, block: () -> Unit) { @@ -372,21 +395,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_0 // touch at 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) } fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -394,21 +429,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_90 // touch at 0 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 90 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 180 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 270 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) } fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -416,21 +463,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_270 // touch at 0 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 90 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 180 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 270 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index d095add1c660..e7138ab11cbf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -757,6 +757,38 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + // Start shade collapse with swipe up + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + + // simulate touch that does not exceed touch slop + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + mNotificationPanelViewController.setTouchSlopExceeded(false); + + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + // fling should still be called after a touch that does not exceed touch slop + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + } + + @Test public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index eb34561d15a0..cc45cf88fa18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.TextAnimator +import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule import org.junit.Test @@ -55,7 +56,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) - verify(mockTextAnimator).glyphFilter = null + verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) } @@ -66,7 +67,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() - verify(mockTextAnimator, times(2)).glyphFilter = null + verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null) verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 340bc96f80c2..3ff7639e9262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -674,7 +674,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should + // and is new enough to do so assertFalse(mEntry.hasInterrupted()) + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -690,8 +692,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { - // GIVEN that mEntry has alerted in the past + // GIVEN that mEntry has alerted in the past, even if it's new mEntry.setInterruption() + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -725,6 +728,27 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mEntry) } + @Test + fun testOnRankingApplied_entryUpdatedButTooOld() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it was actually added 10s ago + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000) + + // WHEN it is updated to HUN and then a ranking update occurs + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index fd1fdce4cf77..a55b11838c7b 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -61,6 +61,7 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELECOM; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.content.pm.PackageManager.MATCH_ALL; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -10633,10 +10634,18 @@ public class NotificationManagerService extends SystemService { private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter> mRequestedNotificationListeners = new ArrayMap<>(); + private final boolean mIsHeadlessSystemUserMode; public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, IPackageManager pm) { + this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode()); + } + + @VisibleForTesting + public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, + IPackageManager pm, boolean isHeadlessSystemUserMode) { super(context, lock, userProfiles, pm); + this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode; } @Override @@ -10661,10 +10670,16 @@ public class NotificationManagerService extends SystemService { if (TextUtils.isEmpty(listeners[i])) { continue; } + int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + // In the headless system user mode, packages might not be installed for the + // system user. Match packages for any user since apps can be installed only for + // non-system users and would be considering uninstalled for the system user. + if (mIsHeadlessSystemUserMode) { + packageQueryFlags += MATCH_ANY_USER; + } ArraySet<ComponentName> approvedListeners = - this.queryPackageForServices(listeners[i], - MATCH_DIRECT_BOOT_AWARE - | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM); + this.queryPackageForServices(listeners[i], packageQueryFlags, + USER_SYSTEM); for (int k = 0; k < approvedListeners.size(); k++) { ComponentName cn = approvedListeners.valueAt(k); addDefaultComponentOrPackage(cn.flattenToString()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index c5131c8f8c1d..101a5cb49f9b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -30,9 +31,11 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -49,6 +52,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.pm.VersionedPackage; +import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerFilter; @@ -69,6 +73,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.internal.util.reflection.FieldSetter; @@ -77,6 +82,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.Arrays; import java.util.List; public class NotificationListenersTest extends UiServiceTestCase { @@ -85,6 +91,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private PackageManager mPm; @Mock private IPackageManager miPm; + @Mock + private Resources mResources; @Mock NotificationManagerService mNm; @@ -96,7 +104,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); - + private ComponentName mUninstalledComponent = new ComponentName("pkg3", + "pkg3.NotificationListenerService"); @Before public void setUp() throws Exception { @@ -111,7 +120,7 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testReadExtraTag() throws Exception { - String xml = "<" + TAG_REQUESTED_LISTENERS+ ">" + String xml = "<" + TAG_REQUESTED_LISTENERS + ">" + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">" + "<allowed types=\"7\" />" + "</listener>" @@ -131,11 +140,55 @@ public class NotificationListenersTest extends UiServiceTestCase { } @Test + public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception { + // setup with headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ true)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent); + } + + @Test + public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled() + throws Exception { + // setup without headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ false)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent); + } + + private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) { + ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName)); + when(mResources + .getString( + com.android.internal.R.string.config_defaultListenerAccessPackages)) + .thenReturn(componentName.getPackageName()); + when(mContext.getResources()).thenReturn(mResources); + doReturn(components).when(mListeners).queryPackageForServices( + eq(componentName.getPackageName()), + intThat(hasIntBitFlag(MATCH_ANY_USER)), + anyInt()); + } + + public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) { + return arg -> arg != null && ((arg & flag) == flag); + } + + @Test public void testWriteExtraTag() throws Exception { NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); VersionedPackage a1 = new VersionedPackage("pkg1", 243); NotificationListenerFilter nlf2 = - new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1})); + new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1})); mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java deleted file mode 100644 index d7650420788c..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java +++ /dev/null @@ -1,551 +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.server.notification; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.app.RemoteInput; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.StyleSpan; -import android.util.Pair; -import android.widget.RemoteViews; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.UiServiceTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NotificationTest extends UiServiceTestCase { - - @Mock - ActivityManager mAm; - - @Mock - Resources mResources; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testDoesNotStripsExtenders() { - Notification.Builder nb = new Notification.Builder(mContext, "channel"); - nb.extend(new Notification.CarExtender().setColor(Color.RED)); - nb.extend(new Notification.TvExtender().setChannelId("different channel")); - nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); - Notification before = nb.build(); - Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); - - assertTrue(before == after); - - assertEquals("different channel", new Notification.TvExtender(before).getChannelId()); - assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); - assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyles() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyleToStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_styleToNoStyle() { - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_changeStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle()); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testInboxTextChange() { - Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); - Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); - } - - @Test - public void testBigTextTextChange() { - Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("something")); - Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("else")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); - } - - @Test - public void testBigPictureChange() { - Bitmap bitA = mock(Bitmap.class); - when(bitA.getGenerationId()).thenReturn(100); - Bitmap bitB = mock(Bitmap.class); - when(bitB.getGenerationId()).thenReturn(200); - - Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); - Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); - } - - @Test - public void testMessagingChange_text() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class))) - .addMessage(new Notification.MessagingStyle.Message( - "b", 100, mock(Person.class))) - ); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_data() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)) - .setData("text", mock(Uri.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_sender() { - Person a = mock(Person.class); - when(a.getName()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getName()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_key() { - Person a = mock(Person.class); - when(a.getKey()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getKey()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_ignoreTimeChange() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 1000, mock(Person.class))) - ); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testRemoteViews_nullChange() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test"); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(189); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(2); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(1); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testActionsDifferent_null() { - Notification n1 = new Notification.Builder(mContext, "test") - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSame() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentText() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSpannables() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, - new SpannableStringBuilder().append("test1", - new StyleSpan(Typeface.BOLD), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), - intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentNumber() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentIntent() { - PendingIntent intent1 = mock(PendingIntent.class); - PendingIntent intent2 = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsIgnoresRemoteInputs() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"i", "m"}) - .build()) - .build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"t", "m"}) - .build()) - .build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testFreeformRemoteInputActionPair_noRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(false)); - } - - @Test - public void testFreeformRemoteInputActionPair_hasRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = new RemoteInput.Builder("a").build(); - - Notification.Action actionWithRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(remoteInput) - .build(); - - Notification.Action actionWithoutRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutRemoteInput) - .addAction(actionWithRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(false); - - assertNotNull(remoteInputActionPair); - assertEquals(remoteInput, remoteInputActionPair.first); - assertEquals(actionWithRemoteInput, remoteInputActionPair.second); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput( - new RemoteInput.Builder("a") - .setAllowFreeFormInput(false).build()) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(true)); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = - new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); - RemoteInput freeformRemoteInput = - new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); - - Notification.Action actionWithFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(freeformRemoteInput) - .build(); - - Notification.Action actionWithoutFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .addRemoteInput(remoteInput) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutFreeformRemoteInput) - .addAction(actionWithFreeformRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(true); - - assertNotNull(remoteInputActionPair); - assertEquals(freeformRemoteInput, remoteInputActionPair.first); - assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); - } -} - |