From cf13fe7776bf308e57540e7ee37e0f5022aa33e2 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 25 Jul 2024 15:46:03 +0800 Subject: [PATCH 01/10] Create SubscriptionRepository.phoneNumberFlow And use it in the MobileNetworkSettings. Fix: 341318273 Flag: EXEMPT bug fix Test: manual - on MobileNetworkSettings Test: unit test Change-Id: I886373c1ed5129ebd8bcceedd513e9d1776106c8 --- ...eNetworkPhoneNumberPreferenceController.kt | 91 ++++--------------- .../telephony/MobileNetworkSettings.java | 2 +- .../telephony/SubscriptionRepository.kt | 44 ++++++--- ...workPhoneNumberPreferenceControllerTest.kt | 88 +++++++----------- .../telephony/SubscriptionRepositoryTest.kt | 16 ++++ 5 files changed, 100 insertions(+), 141 deletions(-) diff --git a/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt b/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt index 10a8b53e5d4..db16acdfc59 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt @@ -17,49 +17,37 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager -import android.util.Log -import androidx.annotation.VisibleForTesting -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceScreen import com.android.settings.R import com.android.settings.flags.Flags -import com.android.settings.network.SubscriptionInfoListViewModel import com.android.settings.network.SubscriptionUtil import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -/** - * Preference controller for "Phone number" - */ -class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String) : - TelephonyBasePreferenceController(context, key) { +/** Preference controller for "Phone number" */ +class MobileNetworkPhoneNumberPreferenceController +@JvmOverloads +constructor( + context: Context, + key: String, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), +) : TelephonyBasePreferenceController(context, key) { - private lateinit var lazyViewModel: Lazy private lateinit var preference: Preference - private var phoneNumber = String() - - fun init(fragment: Fragment, subId: Int) { - lazyViewModel = fragment.viewModels() + fun init(subId: Int) { mSubId = subId } - override fun getAvailabilityStatus(subId: Int): Int = when { - !Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE - SubscriptionManager.isValidSubscriptionId(subId) - && SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE - else -> CONDITIONALLY_UNAVAILABLE - } + override fun getAvailabilityStatus(subId: Int): Int = + when { + !Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE + SubscriptionManager.isValidSubscriptionId(subId) && + SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE + else -> CONDITIONALLY_UNAVAILABLE + } override fun displayPreference(screen: PreferenceScreen) { super.displayPreference(screen) @@ -67,51 +55,10 @@ class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String } override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { - if (!this::lazyViewModel.isInitialized) { - Log.e( - this.javaClass.simpleName, - "lateinit property lazyViewModel has not been initialized" - ) - return - } - val viewModel by lazyViewModel - val coroutineScope = viewLifecycleOwner.lifecycleScope - - viewModel.subscriptionInfoListFlow - .map { subscriptionInfoList -> - subscriptionInfoList - .firstOrNull { subInfo -> subInfo.subscriptionId == mSubId } + subscriptionRepository.phoneNumberFlow(mSubId).collectLatestWithLifecycle( + viewLifecycleOwner) { phoneNumber -> + preference.summary = phoneNumber ?: getStringUnknown() } - .flowOn(Dispatchers.Default) - .collectLatestWithLifecycle(viewLifecycleOwner) { - it?.let { - coroutineScope.launch { - refreshData(it) - } - } - } - } - - @VisibleForTesting - suspend fun refreshData(subscriptionInfo: SubscriptionInfo){ - withContext(Dispatchers.Default) { - phoneNumber = getFormattedPhoneNumber(subscriptionInfo) - } - refreshUi() - } - - private fun refreshUi(){ - preference.summary = phoneNumber - } - - private fun getFormattedPhoneNumber(subscriptionInfo: SubscriptionInfo?): String { - val phoneNumber = SubscriptionUtil.getBidiFormattedPhoneNumber( - mContext, - subscriptionInfo - ) - return phoneNumber - ?.let { return it.ifEmpty { getStringUnknown() } } - ?: getStringUnknown() } private fun getStringUnknown(): String { diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 896eac6197a..9db5af2b152 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -257,7 +257,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId); use(MobileNetworkSpnPreferenceController.class).init(this, mSubId); - use(MobileNetworkPhoneNumberPreferenceController.class).init(this, mSubId); + use(MobileNetworkPhoneNumberPreferenceController.class).init(mSubId); use(MobileNetworkImeiPreferenceController.class).init(this, mSubId); final MobileDataPreferenceController mobileDataPreferenceController = diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index c95231041d0..cc8c8b47b2d 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -24,13 +24,14 @@ import androidx.lifecycle.LifecycleOwner import com.android.settings.network.SubscriptionUtil import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -52,7 +53,7 @@ class SubscriptionRepository(private val context: Context) { /** Flow of whether the subscription enabled for the given [subId]. */ fun isSubscriptionEnabledFlow(subId: Int): Flow { if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) - return context.subscriptionsChangedFlow() + return subscriptionsChangedFlow() .map { subscriptionManager.isSubscriptionEnabled(subId) } .conflate() .onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") } @@ -87,12 +88,30 @@ class SubscriptionRepository(private val context: Context) { }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) /** Flow of active subscription ids. */ - fun activeSubscriptionIdListFlow(): Flow> = context.subscriptionsChangedFlow() - .map { subscriptionManager.activeSubscriptionIdList.sorted() } - .distinctUntilChanged() - .conflate() - .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") } - .flowOn(Dispatchers.Default) + fun activeSubscriptionIdListFlow(): Flow> = + subscriptionsChangedFlow() + .map { subscriptionManager.activeSubscriptionIdList.sorted() } + .distinctUntilChanged() + .conflate() + .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") } + .flowOn(Dispatchers.Default) + + fun activeSubscriptionInfoFlow(subId: Int): Flow = + subscriptionsChangedFlow() + .map { subscriptionManager.getActiveSubscriptionInfo(subId) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) + + @OptIn(ExperimentalCoroutinesApi::class) + fun phoneNumberFlow(subId: Int): Flow = + activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo -> + if (subInfo != null) { + context.phoneNumberFlow(subInfo) + } else { + flowOf(null) + } + } } val Context.subscriptionManager: SubscriptionManager? @@ -100,9 +119,12 @@ val Context.subscriptionManager: SubscriptionManager? fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!! -fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map { - SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) -}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default) +fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo): Flow = + subscriptionsChangedFlow() + .map { SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) fun Context.subscriptionsChangedFlow(): Flow = SubscriptionRepository(this).subscriptionsChangedFlow() diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt index 38c47c28ccc..f56c0c4b351 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt @@ -17,8 +17,7 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.SubscriptionInfo -import androidx.fragment.app.Fragment +import androidx.lifecycle.testing.TestLifecycleOwner import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider @@ -26,17 +25,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settings.core.BasePreferenceController -import com.android.settings.network.SubscriptionInfoListViewModel import com.android.settings.network.SubscriptionUtil import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoSession -import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.stub import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -44,29 +45,25 @@ import org.mockito.quality.Strictness class MobileNetworkPhoneNumberPreferenceControllerTest { private lateinit var mockSession: MockitoSession - private val mockViewModels = mock>() - private val mockFragment = mock{ - val viewmodel = mockViewModels - } - - private var mockPhoneNumber = String() private val context: Context = ApplicationProvider.getApplicationContext() - private val controller = MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY) + private val mockSubscriptionRepository = mock() + + private val controller = + MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY, mockSubscriptionRepository) private val preference = Preference(context).apply { key = TEST_KEY } private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) @Before fun setUp() { - mockSession = ExtendedMockito.mockitoSession() - .initMocks(this) - .mockStatic(SubscriptionUtil::class.java) - .strictness(Strictness.LENIENT) - .startMocking() + mockSession = + ExtendedMockito.mockitoSession() + .mockStatic(SubscriptionUtil::class.java) + .strictness(Strictness.LENIENT) + .startMocking() preferenceScreen.addPreference(preference) + controller.init(SUB_ID) controller.displayPreference(preferenceScreen) - - whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber) } @After @@ -75,41 +72,29 @@ class MobileNetworkPhoneNumberPreferenceControllerTest { } @Test - fun refreshData_getEmptyPhoneNumber_preferenceIsNotVisible() = runBlocking { + fun onViewCreated_cannotGetPhoneNumber_displayUnknown() = runBlocking { whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true) - whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn( - listOf( - SUB_INFO_1, - SUB_INFO_2 - ) - ) - var mockSubId = 2 - controller.init(mockFragment, mockSubId) - mockPhoneNumber = String() + mockSubscriptionRepository.stub { + on { phoneNumberFlow(SUB_ID) } doReturn flowOf(null) + } - controller.refreshData(SUB_INFO_2) + controller.onViewCreated(TestLifecycleOwner()) + delay(100) - assertThat(preference.summary).isEqualTo( - context.getString(R.string.device_info_default)) + assertThat(preference.summary).isEqualTo(context.getString(R.string.device_info_default)) } @Test - fun refreshData_getPhoneNumber_preferenceSummaryIsExpected() = runBlocking { + fun onViewCreated_canGetPhoneNumber_displayPhoneNumber() = runBlocking { whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true) - whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn( - listOf( - SUB_INFO_1, - SUB_INFO_2 - ) - ) - var mockSubId = 2 - controller.init(mockFragment, mockSubId) - mockPhoneNumber = "test phone number" - whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber) + mockSubscriptionRepository.stub { + on { phoneNumberFlow(SUB_ID) } doReturn flowOf(PHONE_NUMBER) + } - controller.refreshData(SUB_INFO_2) + controller.onViewCreated(TestLifecycleOwner()) + delay(100) - assertThat(preference.summary).isEqualTo(mockPhoneNumber) + assertThat(preference.summary).isEqualTo(PHONE_NUMBER) } @Test @@ -123,18 +108,7 @@ class MobileNetworkPhoneNumberPreferenceControllerTest { private companion object { const val TEST_KEY = "test_key" - const val DISPLAY_NAME_1 = "Sub 1" - const val DISPLAY_NAME_2 = "Sub 2" - - val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply { - setId(1) - setDisplayName(DISPLAY_NAME_1) - }.build() - - val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply { - setId(2) - setDisplayName(DISPLAY_NAME_2) - }.build() - + const val SUB_ID = 10 + const val PHONE_NUMBER = "1234567890" } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt index 75c9aa14456..f75c14a6d37 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt @@ -204,6 +204,22 @@ class SubscriptionRepositoryTest { assertThat(phoneNumber).isEqualTo(NUMBER_1) } + @Test + fun phoneNumberFlow_withSubId() = runBlocking { + val subInfo = SubscriptionInfo.Builder().apply { + setId(SUB_ID_IN_SLOT_1) + setMcc(MCC) + }.build() + mockSubscriptionManager.stub { + on { getActiveSubscriptionInfo(SUB_ID_IN_SLOT_1) } doReturn subInfo + on { getPhoneNumber(SUB_ID_IN_SLOT_1) } doReturn NUMBER_1 + } + + val phoneNumber = repository.phoneNumberFlow(SUB_ID_IN_SLOT_1).firstWithTimeoutOrNull() + + assertThat(phoneNumber).isEqualTo(NUMBER_1) + } + private companion object { const val SIM_SLOT_INDEX_0 = 0 const val SUB_ID_IN_SLOT_0 = 2 From a0ba9182ada1a43816c93afef2d9b7ac1dfd3695 Mon Sep 17 00:00:00 2001 From: Hakjun Choi Date: Mon, 29 Jul 2024 16:46:38 +0000 Subject: [PATCH 02/10] [Internal cleanup] remove warning message for resource ID Currently we are seeing Drawable com.android.settings:drawable/ic_check_circle_24px has unresolved theme attributes! error To address this, use Context.getDrawable(int) to provide theme info when using the icon Bug: 356149221 Flag: EXEMPT bugfix Test: manually run Satellite setting menu and check the log whether the waring is shown or not Change-Id: I55fd1bd4cbaf07e7433cdd286e71f88dacc3e1b5 --- .../android/settings/network/telephony/SatelliteSetting.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/network/telephony/SatelliteSetting.java b/src/com/android/settings/network/telephony/SatelliteSetting.java index 7e9e61d07ed..df580482ed0 100644 --- a/src/com/android/settings/network/telephony/SatelliteSetting.java +++ b/src/com/android/settings/network/telephony/SatelliteSetting.java @@ -150,7 +150,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment { /* In case satellite is allowed by carrier's entitlement server, the page will show the check icon with guidance that satellite is included in user's mobile plan */ preference.setTitle(R.string.title_have_satellite_plan); - icon = getResources().getDrawable(R.drawable.ic_check_circle_24px); + icon = getContext().getDrawable(R.drawable.ic_check_circle_24px); } else { /* Or, it will show the blocked icon with the guidance that satellite is not included in user's mobile plan */ From b8b897e552dc1dfd5edf422c630b662ee1ee250d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Mon, 29 Jul 2024 17:45:27 +0200 Subject: [PATCH 03/10] Improve lifecycle of ZenModeFragment & friends * Don't keep Settings observers longer than start-stop. * Only call updateState() once on controllers during create->start->resume. * Remove some duplicate controller update methods from ZenModesFragmentBase (we can directly call DashboardFragment's). * Don't update controllers if unrelated modes were changed. * Extract ZenSettingsObserver for use in the link tile later. * Add tests. Fixes: 353946788 Test: atest com.android.settings.notification.modes Flag: android.app.modes_ui Change-Id: I64b51714d699b5c3a592a76fcb615d2999998829 --- .../AbstractZenModePreferenceController.java | 43 ++- .../ManualDurationPreferenceController.java | 7 +- .../ZenModeEditNameIconFragmentBase.java | 56 +-- .../notification/modes/ZenModeFragment.java | 22 +- .../modes/ZenModeFragmentBase.java | 148 +++---- ...ModeTriggerUpdatePreferenceController.java | 9 +- .../modes/ZenModesFragmentBase.java | 67 +--- .../modes/ZenModesListFragment.java | 2 +- .../modes/ZenSettingsObserver.java | 68 ++++ .../robotests/res/xml/modes_fake_settings.xml | 22 ++ .../modes/ZenModeFragmentBaseTest.java | 364 ++++++++++++++++++ 11 files changed, 577 insertions(+), 231 deletions(-) create mode 100644 src/com/android/settings/notification/modes/ZenSettingsObserver.java create mode 100644 tests/robotests/res/xml/modes_fake_settings.xml create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java index c740847af0e..c47345659fb 100644 --- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java +++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java @@ -25,8 +25,8 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.notification.modes.ZenMode; @@ -92,29 +92,14 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon return true; } - // Called by parent Fragment onAttach, for any methods (such as isAvailable()) that need - // zen mode info before onStart. Most callers should use updateZenMode instead, which will - // do any further necessary propagation. - protected final void setZenMode(@NonNull ZenMode zenMode) { + /** + * Assigns the {@link ZenMode} of this controller, so that it can be used later from + * {@link #isAvailable()} and {@link #updateState(Preference)}. + */ + final void setZenMode(@NonNull ZenMode zenMode) { mZenMode = zenMode; } - // Called by the parent Fragment onStart, which means it will happen before resume. - public void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) { - mZenMode = zenMode; - updateState(preference); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - if (mZenMode != null) { - displayPreference(screen, mZenMode); - } - } - - public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {} - @Override public final void updateState(Preference preference) { super.updateState(preference); @@ -167,4 +152,20 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon return mode; }); } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Nullable + ZenMode getZenMode() { + return mZenMode; + } + + /** + * Convenience method for tests. Assigns the {@link ZenMode} of this controller, and calls + * {@link #updateState(Preference)} immediately. + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + final void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) { + mZenMode = zenMode; + updateState(preference); + } } diff --git a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java index 073f8ab78f8..28aac639d6c 100644 --- a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java +++ b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification.modes; import android.content.Context; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -49,12 +50,12 @@ public class ManualDurationPreferenceController extends AbstractZenModePreferenc return zenMode.isManualDnd(); } - // Called by parent fragment onAttach(). + // Called by parent fragment onStart(). void registerSettingsObserver() { mSettingsObserver.register(); } - // Called by parent fragment onDetach(). + // Called by parent fragment onStop(). void unregisterSettingsObserver() { mSettingsObserver.unregister(); } @@ -69,7 +70,7 @@ public class ManualDurationPreferenceController extends AbstractZenModePreferenc } @Override - public void updateState(Preference preference, ZenMode unusedZenMode) { + public void updateState(Preference preference, @NonNull ZenMode unusedZenMode) { // This controller is a link between a Settings value (ZEN_DURATION) and the manual DND // mode. The status of the zen mode object itself doesn't affect the preference // value, as that comes from settings; that value from settings will determine the diff --git a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java index d666254cfb8..96cbf91b0d3 100644 --- a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java @@ -21,14 +21,11 @@ import static com.google.common.base.Preconditions.checkState; import android.content.Context; import android.os.Bundle; -import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -39,7 +36,6 @@ import com.android.settingslib.notification.modes.ZenModesBackend; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; -import java.util.Collection; import java.util.List; /** @@ -79,7 +75,11 @@ public abstract class ZenModeEditNameIconFragmentBase extends DashboardFragment ? icicle.getParcelable(MODE_KEY, ZenMode.class) : onCreateInstantiateZenMode(); - if (mZenMode == null) { + if (mZenMode != null) { + for (var controller : getZenPreferenceControllers()) { + controller.setZenMode(mZenMode); + } + } else { finish(); } } @@ -110,58 +110,32 @@ public abstract class ZenModeEditNameIconFragmentBase extends DashboardFragment ); } + private Iterable getZenPreferenceControllers() { + return getPreferenceControllers().stream() + .flatMap(List::stream) + .filter(AbstractZenModePreferenceController.class::isInstance) + .map(AbstractZenModePreferenceController.class::cast) + .toList(); + } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Nullable ZenMode getZenMode() { return mZenMode; } - @Override - public void onStart() { - super.onStart(); - updateControllers(); - } - @VisibleForTesting final void setModeName(String name) { checkNotNull(mZenMode).getRule().setName(Strings.nullToEmpty(name)); - updateControllers(); // Updates confirmation button. + forceUpdatePreferences(); // Updates confirmation button. } @VisibleForTesting final void setModeIcon(@DrawableRes int iconResId) { checkNotNull(mZenMode).getRule().setIconResId(iconResId); - updateControllers(); // Updates icon at the top. + forceUpdatePreferences(); // Updates icon at the top. } - protected void updateControllers() { - PreferenceScreen screen = getPreferenceScreen(); - Collection> controllers = getPreferenceControllers(); - if (mZenMode == null || screen == null || controllers == null) { - return; - } - for (List list : controllers) { - for (AbstractPreferenceController controller : list) { - try { - final String key = controller.getPreferenceKey(); - final Preference preference = screen.findPreference(key); - if (preference != null) { - AbstractZenModePreferenceController zenController = - (AbstractZenModePreferenceController) controller; - zenController.updateZenMode(preference, mZenMode); - } else { - Log.d(getLogTag(), - String.format("Cannot find preference with key %s in Controller %s", - key, controller.getClass().getSimpleName())); - } - controller.displayPreference(screen); - } catch (ClassCastException e) { - // Skip any controllers that aren't AbstractZenModePreferenceController. - Log.d(getLogTag(), "Could not cast: " + controller.getClass().getSimpleName()); - } - } - } - } @VisibleForTesting final void saveMode() { diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 5aeb34d90e4..1b7e3444b22 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -79,14 +79,6 @@ public class ZenModeFragment extends ZenModeFragmentBase { return prefControllers; } - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - // allow duration preference controller to listen for settings changes - use(ManualDurationPreferenceController.class).registerSettingsObserver(); - } - @Override public void onStart() { super.onStart(); @@ -99,6 +91,9 @@ public class ZenModeFragment extends ZenModeFragmentBase { mModeMenuProvider = new ModeMenuProvider(mode); activity.addMenuProvider(mModeMenuProvider); } + + // allow duration preference controller to listen for settings changes + use(ManualDurationPreferenceController.class).registerSettingsObserver(); } @Override @@ -106,13 +101,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { if (getActivity() != null) { getActivity().removeMenuProvider(mModeMenuProvider); } - super.onStop(); - } - - @Override - public void onDetach() { use(ManualDurationPreferenceController.class).unregisterSettingsObserver(); - super.onDetach(); + super.onStop(); } @Override @@ -122,13 +112,13 @@ public class ZenModeFragment extends ZenModeFragmentBase { } @Override - protected void updateZenModeState() { + protected void onUpdatedZenModeState() { // Because this fragment may be asked to finish by the delete menu but not be done doing // so yet, ignore any attempts to update info in that case. if (getActivity() != null && getActivity().isFinishing()) { return; } - super.updateZenModeState(); + super.onUpdatedZenModeState(); } private class ModeMenuProvider implements MenuProvider { diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index f461fc3511c..c63b3a8c10b 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -18,24 +18,18 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; -import android.content.Context; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.notification.modes.ZenMode; -import com.google.common.base.Preconditions; - import java.util.List; -import java.util.function.Consumer; /** * Base class for Settings pages used to configure individual modes. @@ -43,13 +37,27 @@ import java.util.function.Consumer; abstract class ZenModeFragmentBase extends ZenModesFragmentBase { static final String TAG = "ZenModeSettings"; - @Nullable // only until reloadMode() is called - private ZenMode mZenMode; + @Nullable private ZenMode mZenMode; + @Nullable private ZenMode mModeOnLastControllerUpdate; @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); + public void onCreate(Bundle icicle) { + mZenMode = loadModeFromArguments(); + if (mZenMode != null) { + // Propagate mode info through to controllers. Must be done before super.onCreate(), + // because that one calls AbstractPreferenceController.isAvailable(). + for (var controller : getZenPreferenceControllers()) { + controller.setZenMode(mZenMode); + } + } else { + toastAndFinish(); + } + super.onCreate(icicle); + } + + @Nullable + private ZenMode loadModeFromArguments() { String id = null; if (getActivity() != null && getActivity().getIntent() != null) { id = getActivity().getIntent().getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID); @@ -60,93 +68,65 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { } if (id == null) { Log.d(TAG, "No id provided"); - toastAndFinish(); - return; + return null; } - if (!reloadMode(id)) { - Log.d(TAG, "Mode id " + id + " not found"); - toastAndFinish(); - return; - } - if (mZenMode != null) { - // Propagate mode info through to controllers. - for (List list : getPreferenceControllers()) { - try { - for (AbstractPreferenceController controller : list) { - // mZenMode guaranteed non-null from reloadMode() above - ((AbstractZenModePreferenceController) controller).setZenMode(mZenMode); - } - } catch (ClassCastException e) { - // ignore controllers that aren't AbstractZenModePreferenceController - } - } + + ZenMode mode = mBackend.getMode(id); + if (mode == null) { + Log.d(TAG, "Mode with id " + id + " not found"); + return null; } + return mode; } - /** - * Refresh stored ZenMode data. - * @param id the mode ID - * @return whether we successfully got mode data from the backend. - */ - private boolean reloadMode(String id) { - mZenMode = mBackend.getMode(id); - if (mZenMode == null) { - return false; - } - return true; + private Iterable getZenPreferenceControllers() { + return getPreferenceControllers().stream() + .flatMap(List::stream) + .filter(AbstractZenModePreferenceController.class::isInstance) + .map(AbstractZenModePreferenceController.class::cast) + .toList(); } - /** - * Refresh ZenMode data any time the system's zen mode state changes (either the zen mode value - * itself, or the config), and also (once updated) update the info for all controllers. - */ @Override - protected void updateZenModeState() { + protected void onUpdatedZenModeState() { if (mZenMode == null) { - // This shouldn't happen, but guard against it in case + Log.wtf(TAG, "mZenMode is null in onUpdatedZenModeState"); toastAndFinish(); return; } + String id = mZenMode.getId(); - if (!reloadMode(id)) { + ZenMode mode = mBackend.getMode(id); + if (mode == null) { Log.d(TAG, "Mode id=" + id + " not found"); toastAndFinish(); return; } - updateControllers(); + + mZenMode = mode; + maybeUpdateControllersState(mode); } - private void updateControllers() { - if (getPreferenceControllers() == null || mZenMode == null) { - return; + /** + * Updates all {@link AbstractZenModePreferenceController} based on the loaded mode info. + * For each controller, {@link AbstractZenModePreferenceController#setZenMode} will be called. + * Then, {@link AbstractZenModePreferenceController#updateState} will be called as well, unless + * we determine it's not necessary (for example, if we know that {@code DashboardFragment} will + * do it soon). + */ + private void maybeUpdateControllersState(@NonNull ZenMode zenMode) { + boolean needsFullUpdate = + getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED) + && (mModeOnLastControllerUpdate == null + || !mModeOnLastControllerUpdate.equals(zenMode)); + mModeOnLastControllerUpdate = zenMode.copy(); + + for (var controller : getZenPreferenceControllers()) { + controller.setZenMode(zenMode); } - final PreferenceScreen screen = getPreferenceScreen(); - if (screen == null) { - Log.d(TAG, "PreferenceScreen not found"); - return; - } - for (List list : getPreferenceControllers()) { - for (AbstractPreferenceController controller : list) { - try { - // Find preference associated with controller - final String key = controller.getPreferenceKey(); - final Preference preference = screen.findPreference(key); - if (preference != null) { - AbstractZenModePreferenceController zenController = - (AbstractZenModePreferenceController) controller; - zenController.updateZenMode(preference, mZenMode); - } else { - Log.d(TAG, - String.format("Cannot find preference with key %s in Controller %s", - key, controller.getClass().getSimpleName())); - } - controller.displayPreference(screen); - } catch (ClassCastException e) { - // Skip any controllers that aren't AbstractZenModePreferenceController. - Log.d(TAG, "Could not cast: " + controller.getClass().getSimpleName()); - } - } + if (needsFullUpdate) { + forceUpdatePreferences(); } } @@ -163,16 +143,4 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { public ZenMode getMode() { return mZenMode; } - - protected final boolean saveMode(Consumer updater) { - Preconditions.checkState(mBackend != null); - ZenMode mode = mZenMode; - if (mode == null) { - Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")"); - return false; - } - updater.accept(mode); - mBackend.updateMode(mode); - return true; - } } diff --git a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java index 043a38c1cf8..3ee6d9443e4 100644 --- a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java @@ -37,7 +37,6 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settingslib.PrimarySwitchPreference; @@ -77,13 +76,6 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference return !zenMode.isCustomManual() && !zenMode.isManualDnd(); } - @Override - public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) { - // Preload approved components, but only for the package that owns the rule (since it's the - // only package that can have a valid configurationActivity). - mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName()); - } - @Override void updateState(Preference preference, @NonNull ZenMode zenMode) { if (!isAvailable(zenMode)) { @@ -137,6 +129,7 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference @SuppressLint("SwitchIntDef") private void setUpForAppTrigger(Preference preference, ZenMode mode) { // App-owned mode may have triggerDescription, configurationActivity, or both/neither. + mServiceListing.loadApprovedComponents(mode.getRule().getPackageName()); Intent configurationIntent = mConfigurationActivityHelper.getConfigurationActivityIntentForMode( mode, mServiceListing::findService); diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java index 0bc06173fab..652415b50e9 100644 --- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java @@ -16,14 +16,11 @@ package com.android.settings.notification.modes; +import static com.google.common.base.Preconditions.checkNotNull; + import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; import android.os.UserManager; -import android.provider.Settings.Global; import android.util.Log; import androidx.annotation.VisibleForTesting; @@ -38,17 +35,10 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { protected static final String TAG = "ZenModesSettings"; protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private final Handler mHandler = new Handler(); - private final SettingsObserver mSettingsObserver = new SettingsObserver(); - protected Context mContext; - protected ZenModesBackend mBackend; protected ZenHelperBackend mHelperBackend; - - // Individual pages must implement this method based on what they should do when - // the device's zen mode state changes. - protected abstract void updateZenModeState(); + private ZenSettingsObserver mSettingsObserver; ZenModesFragmentBase() { super(UserManager.DISALLOW_ADJUST_VOLUME); @@ -69,8 +59,8 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { mContext = context; mBackend = ZenModesBackend.getInstance(context); mHelperBackend = ZenHelperBackend.getInstance(context); + mSettingsObserver = new ZenSettingsObserver(context, this::onUpdatedZenModeState); super.onAttach(context); - mSettingsObserver.register(); } @Override @@ -83,45 +73,20 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { finish(); } } + + onUpdatedZenModeState(); // Maybe, while we weren't observing. + checkNotNull(mSettingsObserver).register(); } + /** + * Called by this fragment when we know or suspect that Zen Modes data or state has changed. + * Individual pages must implement this method to refresh whatever they're displaying. + */ + protected abstract void onUpdatedZenModeState(); + @Override - public void onResume() { - super.onResume(); - updateZenModeState(); - } - - @Override - public void onDetach() { - super.onDetach(); - mSettingsObserver.unregister(); - } - - private final class SettingsObserver extends ContentObserver { - private static final Uri ZEN_MODE_URI = Global.getUriFor(Global.ZEN_MODE); - private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Global.getUriFor( - Global.ZEN_MODE_CONFIG_ETAG); - - private SettingsObserver() { - super(mHandler); - } - - public void register() { - getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); - getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, this); - } - - public void unregister() { - getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, @Nullable Uri uri) { - super.onChange(selfChange, uri); - // Shouldn't have any other URIs trigger this method, but check just in case. - if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) { - updateZenModeState(); - } - } + public void onStop() { + checkNotNull(mSettingsObserver).unregister(); + super.onStop(); } } diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java index be458b331df..a45ca1796f2 100644 --- a/src/com/android/settings/notification/modes/ZenModesListFragment.java +++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java @@ -65,7 +65,7 @@ public class ZenModesListFragment extends ZenModesFragmentBase { } @Override - protected void updateZenModeState() { + protected void onUpdatedZenModeState() { // TODO: b/322373473 -- update any overall description of modes state here if necessary. // Note the preferences linking to individual rules do not need to be updated, as // updateState() is called on all preference controllers whenever the page is resumed. diff --git a/src/com/android/settings/notification/modes/ZenSettingsObserver.java b/src/com/android/settings/notification/modes/ZenSettingsObserver.java new file mode 100644 index 00000000000..a853646fd94 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenSettingsObserver.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; + +import androidx.annotation.Nullable; + +class ZenSettingsObserver extends ContentObserver { + private static final Uri ZEN_MODE_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE); + private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Settings.Global.getUriFor( + Settings.Global.ZEN_MODE_CONFIG_ETAG); + + private final Context mContext; + @Nullable private Runnable mCallback; + + ZenSettingsObserver(Context context) { + this(context, null); + } + + ZenSettingsObserver(Context context, @Nullable Runnable callback) { + super(context.getMainExecutor(), 0); + mContext = context; + setOnChangeListener(callback); + } + + void register() { + mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); + mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, + this); + } + + void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + void setOnChangeListener(@Nullable Runnable callback) { + mCallback = callback; + } + + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + super.onChange(selfChange, uri); + // Shouldn't have any other URIs trigger this method, but check just in case. + if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) { + if (mCallback != null) { + mCallback.run(); + } + } + } +} diff --git a/tests/robotests/res/xml/modes_fake_settings.xml b/tests/robotests/res/xml/modes_fake_settings.xml new file mode 100644 index 00000000000..a5602dcd700 --- /dev/null +++ b/tests/robotests/res/xml/modes_fake_settings.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java new file mode 100644 index 00000000000..21f19ffa6f1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; + +import static com.android.settings.notification.modes.CharSequenceTruth.assertThat; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Flags; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.testing.FragmentScenario; +import androidx.lifecycle.Lifecycle.State; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.notification.modes.TestModeBuilder; +import com.android.settingslib.notification.modes.ZenMode; +import com.android.settingslib.notification.modes.ZenModesBackend; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowLooper; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public class ZenModeFragmentBaseTest { + + private static final Uri SETTINGS_URI = Settings.Global.getUriFor( + Settings.Global.ZEN_MODE_CONFIG_ETAG); + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock ZenModesBackend mBackend; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void fragment_noArguments_finishes() { + when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE); + + FragmentScenario scenario = createScenario(null); + + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + assertThat(fragment.requireActivity().isFinishing()).isTrue(); + }); + + scenario.close(); + } + + @Test + public void fragment_modeDoesNotExist_finishes() { + when(mBackend.getMode(any())).thenReturn(null); + + FragmentScenario scenario = createScenario("mode_id"); + + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + assertThat(fragment.requireActivity().isFinishing()).isTrue(); + }); + + scenario.close(); + } + + @Test + public void fragment_validMode_updatesControllersOnce() { + ZenMode mode = new TestModeBuilder().setId("mode_id").build(); + when(mBackend.getMode("mode_id")).thenReturn(mode); + + FragmentScenario scenario = createScenario("mode_id"); + + scenario.moveToState(State.CREATED).onFragment(fragment -> { + assertThat(fragment.mShowsId.getZenMode()).isEqualTo(mode); + assertThat(fragment.mShowsId.isAvailable()).isTrue(); + assertThat(fragment.mAvailableIfEnabled.getZenMode()).isEqualTo(mode); + assertThat(fragment.mAvailableIfEnabled.isAvailable()).isTrue(); + + verify(fragment.mShowsId, never()).updateState(any(), any()); + verify(fragment.mAvailableIfEnabled, never()).updateState(any(), any()); + }); + + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + Preference preferenceOne = fragment.requirePreference("pref_id"); + assertThat(preferenceOne.getSummary()).isEqualTo("Id is mode_id"); + + verify(fragment.mShowsId).updateState(any(), eq(mode)); + verify(fragment.mAvailableIfEnabled).updateState(any(), eq(mode)); + }); + + scenario.close(); + } + + @Test + public void fragment_onStartToOnStop_hasRegisteredContentObserver() { + when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE); + FragmentScenario scenario = createScenario("id"); + + scenario.moveToState(State.CREATED).onFragment(fragment -> + assertThat(getSettingsContentObservers(fragment)).isEmpty()); + + scenario.moveToState(State.STARTED).onFragment(fragment -> + assertThat(getSettingsContentObservers(fragment)).hasSize(1)); + + scenario.moveToState(State.RESUMED).onFragment(fragment -> + assertThat(getSettingsContentObservers(fragment)).hasSize(1)); + + scenario.moveToState(State.STARTED).onFragment(fragment -> + assertThat(getSettingsContentObservers(fragment)).hasSize(1)); + + scenario.moveToState(State.CREATED).onFragment(fragment -> + assertThat(getSettingsContentObservers(fragment)).isEmpty()); + + scenario.close(); + } + + @Test + public void fragment_onModeUpdatedWithDifferences_updatesControllers() { + ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build(); + when(mBackend.getMode("id")).thenReturn(originalMode); + + FragmentScenario scenario = createScenario("id"); + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + Preference preference = fragment.requirePreference("pref_name"); + assertThat(preference.getSummary()).isEqualTo("Original"); + verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode)); + + // Now, we get a message saying something changed. + ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build(); + when(mBackend.getMode("id")).thenReturn(updatedMode); + getSettingsContentObservers(fragment).stream().findFirst().get() + .dispatchChange(false, SETTINGS_URI); + ShadowLooper.idleMainLooper(); + + // The screen was updated, and only updated once. + assertThat(preference.getSummary()).isEqualTo("Updated"); + verify(fragment.mShowsName, times(1)).updateState(any(), eq(updatedMode)); + }); + + scenario.close(); + } + + @Test + public void fragment_onModeUpdatedWithoutDifferences_setsModeInControllersButNothingElse() { + ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build(); + when(mBackend.getMode("id")).thenReturn(originalMode); + + FragmentScenario scenario = createScenario("id"); + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + Preference preference = fragment.requirePreference("pref_name"); + assertThat(preference.getSummary()).isEqualTo("Original"); + verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode)); + + // Now, we get a message saying something changed, but it was for a different mode. + ZenMode notUpdatedMode = new TestModeBuilder(originalMode).build(); + when(mBackend.getMode("id")).thenReturn(notUpdatedMode); + getSettingsContentObservers(fragment).stream().findFirst().get() + .dispatchChange(false, SETTINGS_URI); + ShadowLooper.idleMainLooper(); + + // The mode instance was updated, but updateState() was not called. + assertThat(preference.getSummary()).isEqualTo("Original"); + assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(notUpdatedMode); + verify(fragment.mShowsName, never()).updateState(any(), same(notUpdatedMode)); + }); + + scenario.close(); + } + + @Test + public void fragment_onFragmentRestart_reloadsMode() { + ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build(); + when(mBackend.getMode("id")).thenReturn(originalMode); + + FragmentScenario scenario = createScenario("id"); + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + Preference preference = fragment.requirePreference("pref_name"); + assertThat(preference.getSummary()).isEqualTo("Original"); + verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode)); + }); + + ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build(); + when(mBackend.getMode("id")).thenReturn(updatedMode); + + scenario.moveToState(State.CREATED).moveToState(State.RESUMED).onFragment(fragment -> { + Preference preference = fragment.requirePreference("pref_name"); + assertThat(preference.getSummary()).isEqualTo("Updated"); + assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(updatedMode); + }); + + scenario.close(); + } + + @Test + public void fragment_onModeDeleted_finishes() { + ZenMode originalMode = new TestModeBuilder().setId("mode_id").build(); + when(mBackend.getMode("mode_id")).thenReturn(originalMode); + + FragmentScenario scenario = createScenario("mode_id"); + scenario.moveToState(State.RESUMED).onFragment(fragment -> { + assertThat(fragment.requireActivity().isFinishing()).isFalse(); + + // Now it's no longer there... + when(mBackend.getMode(any())).thenReturn(null); + getSettingsContentObservers(fragment).stream().findFirst().get() + .dispatchChange(false, SETTINGS_URI); + ShadowLooper.idleMainLooper(); + + assertThat(fragment.requireActivity().isFinishing()).isTrue(); + }); + + scenario.close(); + } + + private FragmentScenario createScenario(@Nullable String modeId) { + Bundle fragmentArgs = null; + if (modeId != null) { + fragmentArgs = new Bundle(); + fragmentArgs.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId); + } + + FragmentScenario scenario = FragmentScenario.launch( + TestableFragment.class, fragmentArgs, 0, State.INITIALIZED); + + scenario.onFragment(fragment -> { + fragment.setBackend(mBackend); // Before onCreate(). + }); + + return scenario; + } + + public static class TestableFragment extends ZenModeFragmentBase { + + private ShowsIdPreferenceController mShowsId; + private ShowsNamePreferenceController mShowsName; + private AvailableIfEnabledPreferenceController mAvailableIfEnabled; + + @Override + protected List createPreferenceControllers(Context context) { + mShowsId = spy(new ShowsIdPreferenceController(context, "pref_id")); + mShowsName = spy(new ShowsNamePreferenceController(context, "pref_name")); + mAvailableIfEnabled = spy( + new AvailableIfEnabledPreferenceController(context, "pref_enabled")); + return ImmutableList.of(mShowsId, mShowsName, mAvailableIfEnabled); + } + + @NonNull + Preference requirePreference(String key) { + Preference preference = getPreferenceScreen().findPreference(key); + checkNotNull(preference, "Didn't find preference with key " + key); + return preference; + } + + ShadowContentResolver getShadowContentResolver() { + return shadowOf(requireActivity().getContentResolver()); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_fake_settings; + } + + @Override + public int getMetricsCategory() { + return 0; + } + } + + private static class ShowsIdPreferenceController extends AbstractZenModePreferenceController { + + ShowsIdPreferenceController(@NonNull Context context, @NonNull String key) { + super(context, key); + } + + @Override + void updateState(Preference preference, @NonNull ZenMode zenMode) { + preference.setSummary("Id is " + zenMode.getId()); + } + } + + private static class ShowsNamePreferenceController extends AbstractZenModePreferenceController { + + ShowsNamePreferenceController(@NonNull Context context, @NonNull String key) { + super(context, key); + } + + @Override + void updateState(Preference preference, @NonNull ZenMode zenMode) { + preference.setSummary(zenMode.getName()); + } + } + + private static class AvailableIfEnabledPreferenceController extends + AbstractZenModePreferenceController { + + AvailableIfEnabledPreferenceController(@NonNull Context context, @NonNull String key) { + super(context, key); + } + + @Override + public boolean isAvailable(@NonNull ZenMode zenMode) { + return zenMode.isEnabled(); + } + + @Override + void updateState(Preference preference, @NonNull ZenMode zenMode) { + preference.setSummary("Enabled is " + zenMode.isEnabled()); + } + } + + private ImmutableList getSettingsContentObservers(Fragment fragment) { + return ImmutableList.copyOf( + shadowOf(fragment.requireActivity().getContentResolver()) + .getContentObservers(SETTINGS_URI)); + } +} From a463b0af7b3241b88e6a2d4d06e8fe574b1d770c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Mon, 29 Jul 2024 19:58:17 +0200 Subject: [PATCH 04/10] Fix a11y issues in the schedule editor * Don't read start / end time as separate labels. * Fix content description of day buttons (ToggleButton's stateDescription is textOn/textOff -- which are the same for this particular button, thus it wasn't possible to know whether a day was selected or not). Fixes: 346396147 Test: manual, with Talkback Flag: android.app.modes_ui Change-Id: If73a791cf9bd62cf17e058c81a8051b3e7fd82ea --- res/layout/modes_set_schedule_layout.xml | 6 ++++-- .../ZenModeSetSchedulePreferenceController.java | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/res/layout/modes_set_schedule_layout.xml b/res/layout/modes_set_schedule_layout.xml index ebb349e014c..d53e2e42a91 100644 --- a/res/layout/modes_set_schedule_layout.xml +++ b/res/layout/modes_set_schedule_layout.xml @@ -48,7 +48,8 @@ app:layout_constrainedWidth="true" app:layout_constraintHorizontal_bias="0" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium" - android:text="@string/zen_mode_start_time" /> + android:text="@string/zen_mode_start_time" + android:importantForAccessibility="no" /> + android:text="@string/zen_mode_end_time" + android:importantForAccessibility="no" /> { From b7cf038679be6791b0d27101cbed633b2ca912d3 Mon Sep 17 00:00:00 2001 From: Bill Yi Date: Mon, 29 Jul 2024 15:14:51 -0700 Subject: [PATCH 05/10] Import translations. DO NOT MERGE ANYWHERE Auto-generated-cl: translation import Change-Id: I6ae0fec262e838110a867b3555a61f1d577482af --- res/values-am/strings.xml | 2 +- res/values-ar/strings.xml | 30 ++++++++++++++-------------- res/values-b+sr+Latn/strings.xml | 2 +- res/values-bn/strings.xml | 10 +++++----- res/values-bs/strings.xml | 4 ++-- res/values-da/strings.xml | 8 ++++---- res/values-es-rUS/strings.xml | 4 ++-- res/values-eu/strings.xml | 4 ++-- res/values-fr-rCA/strings.xml | 6 +++--- res/values-hi/strings.xml | 8 ++++---- res/values-hr/strings.xml | 2 +- res/values-hu/strings.xml | 4 ++-- res/values-iw/strings.xml | 4 ++-- res/values-ja/strings.xml | 2 +- res/values-ko/strings.xml | 4 ++-- res/values-mk/strings.xml | 2 +- res/values-mr/strings.xml | 4 ++-- res/values-ne/strings.xml | 2 +- res/values-nl/strings.xml | 2 +- res/values-or/strings.xml | 2 +- res/values-ru/strings.xml | 34 ++++++++++++++++---------------- res/values-sl/strings.xml | 2 +- res/values-sr/strings.xml | 2 +- res/values-te/strings.xml | 4 ++-- res/values-th/strings.xml | 2 +- res/values-uk/strings.xml | 4 ++-- res/values-vi/strings.xml | 4 ++-- res/values-zh-rCN/strings.xml | 2 +- 28 files changed, 80 insertions(+), 80 deletions(-) diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index 0b7b9758b34..18927a6c21f 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -1403,7 +1403,7 @@ "የሥራ የይለፍ ቃል ያቀናብሩ" "ፒን ያቀናብሩ" "የሥራ ፒን ያቀናብሩ" - "ስርዓተ-ጥለት ያቀናብሩ" + "ሥርዓተ-ጥለት ያቀናብሩ" "ለተጨማሪ ደህንነት ሲባል መሣሪያውን ለመክፈት ስርዓተ ጥለት ያቀናብሩ" "የሥራ ስርዓተ-ጥለት ያቀናብሩ" "የጣት አሻራን ለመጠቀም የይለፍ ቃል ያቀናብሩ" diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 8bacde63250..df6ee6af145 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -404,7 +404,7 @@ "المس زر التشغيل بدون الضغط عليه" "كيفية إعداد بصمة إصبعك" "زر الاستشعار موجود على الجزء الخلفي لهاتفك. استخدِم إصبع السبابة." - "تكون أداة استشعار بصمة الإصبع متوفرة على شاشتك. يمكنك تسجيل بصمة إصبعك في الشاشة التالية." + "تظهر أداة استشعار بصمة الإصبع على شاشتك. يمكنك تسجيل بصمة إصبعك في الشاشة التالية." "بدء" "يمكنك تحريك إصبعك على الشاشة للعثور على أداة الاستشعار. المس مع الاستمرار أداة استشعار بصمة الإصبع." "صورة توضيحية تبرز الجهاز وموقع جهاز استشعار بصمة الإصبع" @@ -597,9 +597,9 @@ "عند إدخال نقش غير صحيح في المحاولة التالية، سيتم حذف هذا المستخدم" "عند إدخال رقم تعريف شخصي غير صحيح في المحاولة التالية، سيتم حذف هذا المستخدم" "عند إدخال كلمة مرور غير صحيحة في المحاولة التالية، سيتم حذف هذا المستخدم" - "عند إدخال نقش غير صحيح في المحاولة التالية، سيتم حذف ملفك الشخصي للعمل وبياناته" - "عند إدخال رقم تعريف شخصي غير صحيح في المحاولة التالية، سيتم حذف ملفك الشخصي للعمل وبياناته" - "عند إدخال كلمة مرور غير صحيحة في المحاولة التالية، سيتم حذف ملفك الشخصي للعمل وبياناته" + "عند إدخال نقش غير صحيح في المحاولة التالية، سيتم حذف ملف العمل الخاص بك وبياناته" + "عند إدخال رقم تعريف شخصي غير صحيح في المحاولة التالية، سيتم حذف ملف العمل الخاص بك وبياناته" + "عند إدخال كلمة مرور غير صحيحة في المحاولة التالية، سيتم حذف ملف العمل الخاص بك وبياناته" "{count,plural, =1{يجب أن تحتوي كلمة المرور على حرف واحد على الأقل}zero{يجب ألّا يقل عدد الحروف عن #}two{يجب ألّا يقل عدد الحروف عن حرفَين}few{يجب ألّا يقل عدد الحروف عن #}many{يجب ألّا يقل عدد الحروف عن #}other{يجب ألّا يقل عدد الحروف عن #}}" "{count,plural, =1{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على رقم واحد على الأقل.}zero{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على # رقم على الأقل.}two{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على رقمَين على الأقل.}few{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على # أرقام على الأقل.}many{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على # رقمًا على الأقل.}other{في حال استخدام الأرقام فقط، يجب أن تحتوي كلمة المرور على # رقم على الأقل.}}" "{count,plural, =1{يجب أن يحتوي رقم التعريف الشخصي على رقم واحد على الأقل.}zero{يجب أن يحتوي رقم التعريف الشخصي على # رقم على الأقل.}two{يجب أن يحتوي رقم التعريف الشخصي على رقمَين على الأقل.}few{يجب أن يحتوي رقم التعريف الشخصي على # أرقام على الأقل.}many{يجب أن يحتوي رقم التعريف الشخصي على # رقمًا على الأقل.}other{يجب أن يحتوي رقم التعريف الشخصي على # رقم على الأقل.}}" @@ -960,7 +960,7 @@ "تم قطع الاتصال بشبكة %1$s." "الصوت والاهتزاز" "الحسابات" - "حسابات الملفات الشخصية للعمل - %s" + "حسابات ملفات العمل - %s" "حسابات الملفات الشخصية" "استنساخ حسابات الملفات الشخصية" "حساب العمل - %s" @@ -1438,8 +1438,8 @@ "للمتابعة، أدخِل رقم التعريف الشخصي للجهاز." "أدخل كلمة مرور الجهاز للمتابعة." "استخدم نقش ملفك الشخصي الخاص بالعمل للمتابعة." - "أدخل رقم التعريف الشخصي لملفك الشخصي للعمل للمتابعة." - "أدخل كلمة مرور ملفك الشخصي للعمل للمتابعة." + "أدخل رقم التعريف الشخصي لملف العمل الخاص بك للمتابعة." + "أدخل كلمة مرور ملف العمل الخاص بك للمتابعة." "استخدام نقش للجهاز لزيادة الأمان" "إدخال رقم تعريف شخصي للجهاز لزيادة الأمان" "إدخال كلمة مرور للجهاز لزيادة الأمان" @@ -1495,7 +1495,7 @@ "استخدام القفل نفسه لملف العمل وشاشة الجهاز" "هل تريد استخدام قفل واحد؟" "سيستخدم جهازك قفل الشاشة لملف العمل. وسيتم تطبيق سياسات العمل على كلا القفلين." - "لا يتوافق قفل شاشة ملف العمل مع متطلبات الأمان في مؤسستك. يمكنك استخدام القفل ذاته لكل من شاشة جهازك وملفك الشخصي للعمل، ولكن ستنطبق أي سياسات متعلقة بقفل ملف العمل على قفل شاشة الجهاز أيضًا." + "لا يتوافق قفل شاشة ملف العمل مع متطلبات الأمان في مؤسستك. يمكنك استخدام القفل ذاته لكل من شاشة جهازك وملف العمل، ولكن ستنطبق أي سياسات متعلقة بقفل ملف العمل على قفل شاشة الجهاز أيضًا." "استخدام قفل واحد" "استخدام قفل واحد" "نفس قفل شاشة الجهاز" @@ -2663,7 +2663,7 @@ "تم" "{count,plural, =1{إضافة الشهادة أو إزالتها}zero{إضافة الشهادات أو إزالتها}two{إضافة الشهادتَين أو إزالتهما}few{إضافة الشهادات أو إزالتها}many{إضافة الشهادات أو إزالتها}other{إضافة الشهادات أو إزالتها}}" "{numberOfCertificates,plural, =1{يتم تثبيت مرجع تصديق واحد من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادة، يُرجى التواصُل مع المشرف.}zero{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}two{يتم تثبيت مرجعَي تصديق من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}few{يتم تثبيت مراجع تصديق من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}many{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}other{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" على جهازك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة الجهاز، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}}" - "{numberOfCertificates,plural, =1{يتم تثبيت مرجع تصديق واحد من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادة، يُرجى التواصُل مع المشرف.}zero{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}two{يتم تثبيت مرجعَي تصديق من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}few{يتم تثبيت مراجع تصديق من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}many{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}other{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملفك الشخصي للعمل، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}}" + "{numberOfCertificates,plural, =1{يتم تثبيت مرجع تصديق واحد من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادة، يُرجى التواصُل مع المشرف.}zero{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}two{يتم تثبيت مرجعَي تصديق من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}few{يتم تثبيت مراجع تصديق من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}many{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}other{يتم تثبيت مرجع تصديق من قِبل \"{orgName}\" لملف العمل الخاص بك، وهذا قد يسمح لمَن أجرى التثبيت بمراقبة النشاط على شبكة العمل، بما في ذلك الرسائل الإلكترونية والتطبيقات والمواقع الإلكترونية الآمنة.\n\nللحصول على مزيد من المعلومات حول هذه الشهادات، يُرجى التواصُل مع المشرف.}}" "يُمكن لأي جهة خارجية مراقبة نشاط الشبكة، بما في ذلك الرسائل الإلكترونية والتطبيقات ومواقع الويب الآمنة.\n\nويُمكن لبيانات اعتماد موثوقة ومثبتة على جهاز الكمبيوتر إتاحة إجراء ذلك." "{count,plural, =1{الاطّلاع على الشهادة}zero{الاطّلاع على الشهادات}two{الاطّلاع على الشهادتَين}few{الاطّلاع على الشهادات}many{الاطّلاع على الشهادات}other{الاطّلاع على الشهادات}}" "مستخدمون متعدِّدون" @@ -3115,7 +3115,7 @@ "مثل الملف الشخصي" "هل تريد استخدام أصوات الملف الشخصي؟" "تأكيد" - "سيستخدم ملفك الشخصي للعمل الأصوات نفسها التي يستخدمها ملفك الشخصي." + "سيستخدم ملف العمل الخاص بك الأصوات نفسها التي يستخدمها ملفك الشخصي." "الإشعارات" "سجلّ الإشعارات، والمحادثات" "المحادثة" @@ -3182,8 +3182,8 @@ "عدم إظهار أي إشعارات" "الإشعارات الحساسة" "عرض المحتوى الحسّاس عند قفل الجهاز" - "الإشعارات الحساسة بالملف الشخصي للعمل" - "عرض المحتوى الحسّاس بالملف الشخصي للعمل عند قفل الجهاز" + "الإشعارات الحساسة بملف العمل" + "عرض المحتوى الحسّاس بملف العمل عند قفل الجهاز" "عرض محتوى الإشعارات كاملاً" "عرض المحتوى الحسّاس عند فتح القفل فقط" "عدم عرض الإشعارات على الإطلاق" @@ -3305,7 +3305,7 @@ "يمكن لهذا التطبيق استخدام أذونات تطبيقك الشخصي %1$s، مثلاً الوصول إلى الموقع الجغرافي أو مساحة التخزين أو جهات الاتصال." "ما مِن تطبيقات مرتبطة." "{count,plural, =1{تم ربط تطبيق واحد}zero{لم يتم ربط أي تطبيق}two{تم ربط تطبيقَين}few{تم ربط # تطبيقات}many{تم ربط # تطبيقًا}other{تم ربط # تطبيق}}" - "لربط هذه التطبيقات، عليك تثبيت تطبيق %1$s في ملفك الشخصي للعمل." + "لربط هذه التطبيقات، عليك تثبيت تطبيق %1$s في ملف العمل الخاص بك." "لربط هذه التطبيقات، عليك تثبيت تطبيق %1$s في ملفك الشخصي." "انقر لتنزيل التطبيق." "الوصول إلى إعداد \"عدم الإزعاج\"" @@ -3717,7 +3717,7 @@ "عليك إعطاء الهاتف لأحد الوالدَين للسماح بتغيير هذا الإعداد." "لمزيد من المعلومات، يمكنك التواصل مع مشرف تكنولوجيا المعلومات." "مزيد من التفاصيل" - "يمكن للمشرف مراقبة التطبيقات والبيانات المرتبطة بالملفات الشخصية للعمل وإدارتها، بما في ذلك الإعدادات والأذونات والدخول إلى المؤسسة ونشاط الشبكة ومعلومات موقع الجهاز." + "يمكن للمشرف مراقبة التطبيقات والبيانات المرتبطة بملفات العمل وإدارتها، بما في ذلك الإعدادات والأذونات والدخول إلى المؤسسة ونشاط الشبكة ومعلومات موقع الجهاز." "يمكن للمشرف مراقبة التطبيقات والبيانات المرتبطة بهذا المستخدم وإدارتها، بما في ذلك الإعدادات والأذونات والدخول إلى المؤسسة ونشاط الشبكة ومعلومات موقع الجهاز." "يمكن للمشرف مراقبة التطبيقات والبيانات المرتبطة بهذا الجهاز وإدارتها، بما في ذلك الإعدادات والأذونات والدخول إلى المؤسسة ونشاط الشبكة ومعلومات موقع الجهاز." "قد يتمكن مشرف الجهاز من الوصول إلى البيانات المرتبطة بهذا الجهاز وإدارة التطبيقات وتغيير إعدادات الجهاز." @@ -4011,7 +4011,7 @@ "‏تم ضبط الخادم الوكيل HTTP العام" "بيانات الاعتماد الموثوقة" "بيانات اعتماد موثوقة في ملفك الشخصي" - "بيانات اعتماد موثوقة في ملفك الشخصي للعمل" + "بيانات اعتماد موثوقة في ملف العمل الخاص بك" "{count,plural, =1{‏شهادة CA واحدة كحد أدنى}zero{‏# شهادة CA كحد أدنى}two{‏شهادتا CA كحد أدنى}few{‏# شهادات CA كحد أدنى}many{‏# شهادة CA كحد أدنى}other{‏# شهادة CA كحد أدنى}}" "يمكن للمشرف قفل الجهاز وإعادة ضبط كلمة المرور" "يمكن للمشرف حذف جميع بيانات الجهاز" diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml index 43a41e440e3..e129c73e6c0 100644 --- a/res/values-b+sr+Latn/strings.xml +++ b/res/values-b+sr+Latn/strings.xml @@ -508,7 +508,7 @@ "Ako zaboravite otključavanje ekrana, administrator ne može da ga resetuje." "Podesite zasebno zaključavanje za poslovne aplikacije" "Ako zaboravite kako da otključate, zatražite od IT administratora da to resetuje" - "Opcije zaključavanja ekrana" + "Opcije otključavanja ekrana" "Opcije otključavanja ekrana" "Otključavanje koje se automatski potvrđuje" "Otključava se automatski ako unesete PIN sa 6 ili više cifara. To je malo manje pouzdano nego da dodirnete Enter da biste potvrdili." diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml index c2cc0802300..e0f8fac5e35 100644 --- a/res/values-bn/strings.xml +++ b/res/values-bn/strings.xml @@ -282,7 +282,7 @@ "বাতিল করুন" "না থাক" - "আমি সম্মতি জানাচ্ছি" + "আমি সম্মত" "আরও" "আপনার ফেস দিয়ে আনলক করুন" "\'ফেস আনলক\' ফিচার ব্যবহার করার অনুমতি দিন" @@ -335,13 +335,13 @@ "\'ফিঙ্গারপ্রিন্ট আনলক\' করতে অনুমতি" "আঙ্গুলের ছাপ ব্যবহার করুন" "\'ফিঙ্গারপ্রিন্ট আনলক\' ফিচার সম্পর্কে আরও জানুন" - "নিয়ন্ত্রণ আপনার কাছেই আছে" + "নিয়ন্ত্রণ আপনার হাতেই রয়েছে" "আপনি ও আপনার সন্তান এটি নিয়ন্ত্রণ করছেন" "মনে রাখবেন" "আপনার ফোন আনলক বা কেনাকাটাগুলির অনুমোদন করতে আপনার আঙ্গুলের ছাপ ব্যবহার করুন। \n\nদ্রষ্টব্য: আপনি এই ডিভাইসটি আনলক করার জন্য আপনার আঙ্গুলের ছাপ ব্যবহার করতে পারবেন না। আরও তথ্যের জন্য, আপনার প্রতিষ্ঠানের প্রশাসকের সাথে যোগাযোগ করুন।" "বাতিল করুন" "না থাক" - "আমি রাজি" + "আমি সম্মত" "ফিঙ্গারপ্রিন্ট এড়িয়ে যাবেন?" "ফিঙ্গারপ্রিন্ট সেট-আপ করতে এক থেকে দুই মিনিট মতো সময় লাগবে। আপনি যদি এড়িয়ে যেতে চান তবে পরে সেটিংসে গিয়ে পরিবর্তন করতে পারবেন।" "এই আইকনটি দেখলে যাচাই করার জন্য আপনার আঙ্গুলের ছাপ ব্যবহার করুন, যেভাবে কোনও অ্যাপে সাইন-ইন করেন অথবা কেনাকাটায় অনুমোদন দেন" @@ -508,8 +508,8 @@ "আপনি স্ক্রিন লক ভুলে গেলে, আইটি অ্যাডমিন তা রিসেট করতে পারবেন না।" "অন্য একটি অফিস লক সেট করুন" "আপনি এই লকের প্যাটার্ন ভুলে গেলে, আপনার আইটি অ্যাডমিনকে এটা রিসেট করতে বলুন" - "স্ক্রিন লক-এর বিকল্প" - "স্ক্রিন লক-এর বিকল্পগুলি" + "স্ক্রিন লকের বিকল্প" + "স্ক্রিন লকের বিকল্প" "পিন অটোমেটিক কনফার্ম করে ডিভাইস আনলক করা" "৬ বা তার বেশি সংখ্যার সঠিক পিন লিখলে অটোমেটিক আনলক করুন। \'এন্টার\' ট্যাপ করে কনফার্ম করার থেকে এই প্রক্রিয়াটি সামান্য কম নিরাপদ।" "সঠিক পিন অটোমেটিক কনফার্ম করুন" diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml index 0adc52c9395..8e72f5bb753 100644 --- a/res/values-bs/strings.xml +++ b/res/values-bs/strings.xml @@ -4693,7 +4693,7 @@ "Obavještenja blicanjem" "Informacije o obavještenjima blicanjem" "Isključeno" - "Uključeno / blicanje kamere" + "Uključeno / blic kamere" "Uključeno / blicanje ekrana" "Uključeno / blicanje kamere i ekrana" "Kamera ili ekran blicaju kada primite obavještenje ili se alarm oglasi" @@ -4701,7 +4701,7 @@ "Oprezno koristite obavještenja blicanjem ako ste osjetljivi na svjetlost" "blicanje, svjetlo, oštećenje sluha, gubitak sluha" "Pregled" - "Blicanje kamere" + "Blic kamere" "Blicanje ekrana" "Boja blicanja ekrana" "Plava" diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 4c0949a5006..5e17d277c1c 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -270,7 +270,7 @@ "Ansigtsoplåsning" "Ansigtsoplåsning til arbejdsprofil" "Sådan konfigurerer du ansigtsoplåsning" - "Konfigurer ansigtsoplåsning" + "Konfigurer ansigts­oplåsning" "Brug dit ansigt til godkendelse" "Start" @@ -309,7 +309,7 @@ "Konfigurer ansigtsoplåsning igen" "Konfigurer ansigtsoplåsning igen" "Gør sikkerheden og registreringen bedre" - "Konfigurer ansigtsoplåsning" + "Konfigurer ansigts­oplåsning" "Slet din aktuelle ansigtsmodel for at konfigurere ansigtsoplåsning igen.\n\nDin ansigtsmodel slettes permanent og forsvarligt.\n\nEfter sletningen skal du bruge din pinkode, dit mønster eller din adgangskode til at låse telefonen op eller til godkendelse i apps." "Slet din aktuelle ansigtsmodel for at konfigurere ansigtsoplåsning igen.\n\nDin ansigtsmodel slettes permanent og forsvarligt.\n\nEfter sletningen skal du bruge dit fingeraftryk, din pinkode, dit mønster eller din adgangskode til at låse telefonen op eller til godkendelse i apps." "Brug ansigtsoplåsning til" @@ -319,7 +319,7 @@ "Bed altid om bekræftelse" "Kræv altid et bekræftelsestrin ved brug af ansigtsoplåsning i apps" "Slet ansigtsmodel" - "Konfigurer ansigtsoplåsning" + "Konfigurer ansigts­oplåsning" "Skal ansigtsmodellen slettes?" "Din ansigtsmodel slettes permanent og på sikker vis.\n\nNår den er slettet, skal du bruge din pinkode, dit mønster eller din adgangskode til at låse din telefon op eller til godkendelse i apps." "Din ansigtsmodel slettes permanent og på sikker vis.\n\nNår den er slettet, skal du bruge din pinkode, dit mønster eller din adgangskode til at låse din telefon op." @@ -1403,7 +1403,7 @@ "Indstil en adgangskode til arbejdsprofilen" "Angiv en pinkode" "Angiv en pinkode til arbejdsprofilen" - "Indstil et mønster" + "Angiv et mønster" "Få ekstra beskyttelse ved at angive et mønster til oplåsning af enheden" "Indstil et mønster til arbejdsprofilen" "Angiv adgangskode for at bruge fingeraftryk" diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml index c4bd550645f..96086ce053d 100644 --- a/res/values-es-rUS/strings.xml +++ b/res/values-es-rUS/strings.xml @@ -414,7 +414,7 @@ "Borrar" "Toca el sensor" "Coloca el dedo en el sensor y levántalo cuando sientas una vibración." - "Mantén toda la huella dactilar en el sensor hasta que notes una vibración" + "Mantén toda la huella dactilar en el sensor hasta que notes una vibración." "Sin presionar el botón, mantén la huella dactilar en el sensor hasta que sientas una vibración.\n\nMueve un poco el dedo cada vez. Esto permite capturar más superficie de tu huella dactilar." "Mantén presionado el sensor de huellas dactilares" "Levanta el dedo y vuelve a tocar" @@ -1468,7 +1468,7 @@ "Levanta el dedo cuando termines." "Une al menos %d puntos. Vuelve a intentarlo." "Patrón registrado" - "Vuelve a trazar el patrón para confirmarlo" + "Vuelve a trazar el patrón para confirmarlo." "Patrón de desbloqueo nuevo" "Confirmar" "Revisar" diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index c38185b34d3..c99fcc70dba 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -250,7 +250,7 @@ "Kokapena" "Erabili kokapena" "Desaktibatuta" - "{count,plural, =1{Aktibatuta: # aplikaziok kokapena atzi dezake}other{Aktibatuta: # aplikaziok kokapena atzi dezakete}}" + "{count,plural, =1{Aktibatuta: # aplikaziok kokapena erabiltzeko baimena du}other{Aktibatuta: # aplikaziok kokapena erabiltzeko baimena dute}}" "Kargatzen…" "Aplikazioek inguruko gailuak erabiltzeko baimena badute, konektatutako gailuen posizio erlatiboa zehatz dezakete." "Aplikazioek eta zerbitzuek ez dute kokapena atzitzeko baimenik. Hala ere, larrialdietarako zenbaki batera deitzen edo mezu bat bidaltzen baduzu, baliteke larrialdi-zerbitzuei gailuaren kokapena bidaltzea." @@ -1351,7 +1351,7 @@ "Laneko profilaren kokapena" "Aplikazioen kokapen-baimenak" "Kokapena desaktibatuta dago" - "{count,plural, =1{#/{total} aplikaziok kokapena atzi dezake}other{#/{total} aplikaziok kokapena atzi dezakete}}" + "{count,plural, =1{#/{total} aplikaziok kokapena erabiltzeko baimena du}other{#/{total} aplikaziok kokapena erabiltzeko baimena dute}}" "Atzitu duten azkenak" "Ikusi guztiak" "Ikusi xehetasunak" diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml index fcf8c2f5fad..7de5fb0ea01 100644 --- a/res/values-fr-rCA/strings.xml +++ b/res/values-fr-rCA/strings.xml @@ -269,7 +269,7 @@ "Configuration requise" "Déverr. par reconn. faciale" "Déverrouillage reconnaissance faciale pour travail" - "Configurer le déverrouillage par reconn. faciale" + "Configurez le déverrouillage par reconn. faciale" "Configurer le Déverrouillage par reconnaissance faciale" "Utiliser son visage pour s\'authentifier" @@ -402,7 +402,7 @@ "Annuler" "Toucher le capteur" "Touchez l\'interrupteur sans l\'enfoncer" - "Comment configurer votre empreinte digitale" + "Configurez votre empreinte digitale" "Il se trouve à l\'arrière de votre téléphone. Utilisez votre index." "Le capteur d\'empreintes digitales se trouve sur votre écran. Vous capturerez votre empreinte digitale au prochain écran." "Commencer" @@ -1468,7 +1468,7 @@ "Relevez le doigt lorsque vous avez terminé" "Reliez au moins %d points. Veuillez réessayer." "Schéma enregistré" - "Redessinez le schéma pour confirmer" + "Retracez le schéma pour confirmer" "Votre nouveau schéma de déverrouillage est le suivant :" "Confirmer" "Redessiner" diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index 58e82adec73..e5b5ac9e79b 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -270,7 +270,7 @@ "फ़ेस अनलॉक" "वर्क प्रोफ़ाइल के लिए फ़ेस अनलॉक की सुविधा" "फ़ेस अनलॉक की सुविधा सेट अप करने का तरीका" - "फे़स अनलॉक की सुविधा सेट अप करें" + "फे़स अनलॉक सेट अप करें" "चेहरे का इस्तेमाल कर पुष्टि करें" "शुरू करें" @@ -404,7 +404,7 @@ "पावर बटन को दबाने के बजाय, इसे बस उंगली से छुएं" "फ़िंगरप्रिंट सेट अप करने का तरीका" "यह आपके फ़ोन के पीछे दिया गया है. अपने अंगूठे के पास की उंगली का उपयोग करें." - "फ़िंगरप्रिंट सेंसर आपकी स्क्रीन पर है. आपको अगली स्क्रीन पर, अपना फ़िंगरप्रिंट कैप्चर करने का विकल्प मिलेगा." + "फ़िंगरप्रिंट सेंसर आपकी स्क्रीन पर है. अगली स्क्रीन पर, आपको अपना फ़िंगरप्रिंट कैप्चर करना होगा." "शुरू करें" "फ़िंगरप्रिंट सेंसर ढूंढने के लिए, स्क्रीन पर अपनी उंगली घुमाएं. फ़िंगरप्रिंट सेंसर को दबाकर रखें." "डिवाइस और उस पर फ़िंगरप्रिंट सेंसर की जगह बताने वाला चित्रण" @@ -435,7 +435,7 @@ "%d प्रतिशत रजिस्टर" "फ़िंगरप्रिंट सेट अप करने की प्रक्रिया %d प्रतिशत तक पूरी हो चुकी है" "फ़िंगरप्रिंट जोड़ा गया" - "डिवाइस को किसी भी समय अनलॉक करने के लिए सेंसर को छुएं" + "कभी भी छूकर अनलॉक करें" "स्क्रीन बंद होने पर भी डिवाइस को अनलॉक करने के लिए, सेंसर को छुएं. हालांकि, इससे डिवाइस के अनजाने में अनलॉक होने की संभावना बढ़ जाती है." "स्क्रीन, अनलॉक करें" "बाद में करें" @@ -2697,7 +2697,7 @@ "उपयोगकर्ता" "अन्य उपयोगकर्ता" "मेहमान मोड में की गई गतिविधि मिट जाए" - "मेहमान मोड से बाहर निकलने पर, उस मोड में की गई गतिविधियों का डेटा मिटा दिया जाए" + "मेहमान मोड से बाहर निकलने पर, उस मोड में की गई गतिविधियों का डेटा मिट जाएगा" "मेहमान मोड की गतिविधि को मिटाना है?" "मेहमान के तौर पर ब्राउज़ किए गए इस सेशन में मौजूद डेटा और इस्तेमाल किए गए ऐप्लिकेशन को मिटा दिया जाएगा. इसके अलावा, आने वाले समय में हर बार मेहमान मोड से बाहर निकलने पर, उस दौरान की गई गतिविधियां मिटा दी जाएंगी" "मिटाएं, मेहमान, गतिविधि, हटाएं, डेटा, वेबसाइट पर आने या जाने वाले, हमेशा के लिए मिटाएं" diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index 877f85e3ba4..6f354f4408b 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -1404,7 +1404,7 @@ "Postavite PIN" "Postavite PIN poslovnog profila" "Postavite uzorak" - "Radi dodatne sigurnosti, postavite uzorak za otključavanje uređaja" + "Da bi uređaj bio još sigurniji, postavite uzorak za otključavanje" "Postavite uzorak poslovnog profila" "Otisak prsta zahtijeva zaporku" "Otisak prsta zahtijeva uzorak" diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index 277dc6fe3e5..6a63056222c 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -2757,7 +2757,7 @@ "Riasztások extrém károkkal járó veszélyekről" "Komoly veszélyek" "Riasztások komoly károkkal járó veszélyekről" - "NARANCS riasztás" + "AMBER-riasztások" "Közlemények kérése elrabolt gyerekekről" "Ismétlés" "Híváskezelő engedélyezése" @@ -2765,7 +2765,7 @@ "Híváskezelő" - "Vezeték nélküli vészjelzések" + "Vezeték nélküli riasztások" "Mobilszolgáltatók" "Hozzáférési pontok nevei" "VoLTE" diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 746108ca94c..637edd7b1db 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -347,7 +347,7 @@ "כשהסמל הזה מופיע, יש להשתמש בטביעת האצבע לצורך אימות במקרים כמו כניסה לאפליקציה או אישור רכישה" "חשוב לזכור" "איך זה עובד" - "הפיצ\'ר \'ביטול הנעילה בטביעת אצבע\' יוצר תבנית ייחודית של טביעת האצבע שלך כדי לאמת את זהותך. תהליך היצירה של תבנית לטביעת האצבע כולל צילום תמונות של טביעת האצבע שלך מזוויות שונות." + "התכונה \'ביטול הנעילה בטביעת אצבע\' יוצרת תבנית ייחודית של טביעת האצבע שלך כדי לאמת את זהותך. תהליך היצירה של תבנית לטביעת האצבע כולל צילום תמונות של טביעת האצבע שלך מזוויות שונות." "הפיצ\'ר \'ביטול הנעילה בטביעת אצבע\' יוצר תבנית ייחודית של טביעת האצבע של הילד או הילדה כדי לאמת את זהותם. כדי ליצור את התבנית הזו לטביעת האצבע במהלך ההגדרה, יהיה עליהם לצלם תמונות של טביעת האצבע מזוויות שונות." "‏לקבלת התוצאות הטובות ביותר, כדאי להשתמש במגן מסך שקיבל אישור Made for Google. יכול להיות שטביעת האצבע שלך לא תעבוד עם מגני מסך אחרים." "‏לקבלת התוצאות הטובות ביותר, כדאי להשתמש במגן מסך בעל אישור Made for Google. טביעת האצבע של הילד או הילדה עלולה לא לעבוד עם מגני מסך אחרים." @@ -468,7 +468,7 @@ "משהו השתבש. אפשר להגדיר את טביעת האצבע מאוחר יותר ב\'הגדרות\'." "אפשר להגדיר את טביעת האצבע מאוחר יותר." "משהו השתבש. אפשר להגדיר את טביעת האצבע מאוחר יותר." - "עוד אחת" + "הוספה של עוד טביעת אבצע" "הבא" "האפשרות לנעילת המסך מושבתת. האדמין של הארגון יוכל למסור לך פרטים נוספים." "עדיין אפשר להשתמש בטביעת האצבע שלך כדי לאשר רכישות וגישה לאפליקציות." diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 754bb0a2280..86fa905ab34 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -3690,7 +3690,7 @@ "2回ひねる動作でカメラアプリを開く" "手首を2回ひねる動作でカメラアプリを開きます" "表示サイズ" - "すべてのサイズを変更します" + "全てのサイズを変更します" "表示間隔、画面のズーム、スケール、拡大縮小" "プレビュー" "縮小" diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 0367364ff1d..747adc47d5b 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -2697,7 +2697,7 @@ "사용자" "다른 사용자" "게스트 활동 삭제" - "게스트 모드 종료 시 모든 게스트 앱 및 데이터 삭제" + "게스트 모드 종료 시 모든 게스트 앱 및 데이터를 삭제합니다." "게스트 활동을 삭제하시겠습니까?" "현재 게스트 세션의 앱과 데이터가 지금 삭제되고 이후 모든 게스트 활동은 게스트 모드를 종료할 때마다 삭제됩니다." "삭제, 게스트, 활동, 제거, 데이터, 방문자, 지우기" @@ -3234,7 +3234,7 @@ "기기가 잠금 해제되어 있을 때 화면 상단에 알림 배너 표시" "모든 ‘%1$s’ 알림" "모든 %1$s 알림" - "{count,plural, =1{매일 알림 약 #개}other{매일 알림 약 #개}}" + "{count,plural, =1{하루에 약 #개의 알림 수신}other{하루에 약 #개의 알림 수신}}" "{count,plural, =1{매주 알림 약 #개}other{매주 알림 약 #개}}" "전송하지 않음" "기기 및 앱 알림" diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml index 1f6a3795c34..52bd31fe36e 100644 --- a/res/values-mk/strings.xml +++ b/res/values-mk/strings.xml @@ -1829,7 +1829,7 @@ "Зголемете со кратенка и троен допир" "За %1$s" "Големина на приказ и текст" - "Променете како да се прикажува текстот" + "Променете како се прикажува текстот" "Предмет: дизајни на балони на топол воздух" "Од: Бил" "Добро утро!\n\nСамо сакав да проверам како оди со дизајните. Дали ќе бидат готови пред да почнеме да ги правиме новите балони?" diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml index d4635b11102..60c4cd3b831 100644 --- a/res/values-mr/strings.xml +++ b/res/values-mr/strings.xml @@ -349,7 +349,7 @@ "हे कसे काम करते" "ते तुम्हीच आहात याची पडताळणी करण्यासाठी फिंगरप्रिंट अनलॉक तुमच्या फिंगरप्रिंटचे युनिक मॉडेल तयार करते. सेटअपदरम्यान हे फिंगरप्रिंट मॉडेल तयार करण्यासाठी, तुम्ही तुमच्या फिंगरप्रिंटच्या वेगवेगळ्या स्थितीमधील इमेज घ्याल." "फिंगरप्रिंट अनलॉक हे तुमच्या लहान मुलाची, ते तेच आहेत याची पडताळणी करण्यासाठी तुमच्या लहान मुलाच्या फिंगरप्रिंटचे युनिक मॉडेल तयार करते. सेटअपदरम्यान हे फिंगरप्रिंट मॉडेल तयार करण्यासाठी, ते त्यांच्या फिंगरप्रिंटच्या वेगवेगळ्या स्थितीमधील इमेज घेतील." - "सर्वोत्तम परिणामांसाठी, Google साठी बनवलेले प्रमाणित असलेले स्क्रीन प्रोटेक्टर वापरा. इतर स्क्रीन प्रोटेक्‍टरसह, तुमचे फिंगरप्रिंट कदाचित काम करणार नाही." + "सर्वोत्तम परिणामांसाठी, Google साठी बनवलेले प्रमाणित स्क्रीन प्रोटेक्टर वापरा. इतर स्क्रीन प्रोटेक्‍टरसह, तुमची फिंगरप्रिंट कदाचित काम करणार नाही." "सर्वोत्तम परिणामांसाठी, Google साठी बनवलेले प्रमाणित असलेले स्क्रीन प्रोटेक्टर वापरा. इतर स्क्रीन प्रोटेक्‍टरसह, तुमच्या लहान मुलाचे फिंगरप्रिंट कदाचित काम करणार नाही." "वॉच अनलॉक" @@ -1468,7 +1468,7 @@ "पूर्ण झाल्यावर बोट सोडा" "कमीत कमी %d बिंदू कनेक्ट करा. पुन्हा प्रयत्न करा." "पॅटर्न रेकॉर्ड झाला" - "खात्री करण्यासाठी पॅटर्न पुन्हा एकदा काढा" + "कन्फर्म करण्यासाठी पॅटर्न पुन्हा काढा" "तुमचा नवीन अनलॉक पॅटर्न" "कन्फर्म करा" "पुन्हा रेखाटा" diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml index 43a7db4be58..295fd942f90 100644 --- a/res/values-ne/strings.xml +++ b/res/values-ne/strings.xml @@ -1420,7 +1420,7 @@ "आफ्नो कार्य प्रोफाइलको PIN हाल्नुहोस्" "पासवर्ड मिलेन" "PIN मिलेन" - "प्याटर्न फेरि कोर्नुहोस्" + "फेरि प्याटर्न हाल्नुहोस्" "चयन अनलक गर्नुहोस्" "पासवर्ड सेट भएको छ" "PIN सेट भएको छ।" diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index e88a7716858..90fb47f204a 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -4570,7 +4570,7 @@ "Alle apps" "Niet toestaan" "Ultra-wideband (UWB)" - "Helpt de relatieve positie van apparaten in de buurt die UWB hebben te bepalen" + "Helpt de relatieve positie te bepalen van apparaten in de buurt die UWB hebben" "Zet de vliegtuigmodus uit om UWB te gebruiken" "UWB is niet beschikbaar op je huidige locatie" "Cameratoegang" diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml index 16223e18c34..c66668bf813 100644 --- a/res/values-or/strings.xml +++ b/res/values-or/strings.xml @@ -508,7 +508,7 @@ "ଯଦି ଆପଣ ଆପଣଙ୍କ ସ୍କ୍ରିନ ଲକ ଭୁଲି ଯାଆନ୍ତି, ଆପଣଙ୍କ IT ଆଡମିନ ତାହା ରିସେଟ କରିପାରିବେ ନାହିଁ।" "ଏକ ଅଲଗା ୱାର୍କ ଲକ ସେଟ କର" "ଆପଣ ଏହି ଲକଟି ଭୁଲିଗଲେ, ଏହାକୁ ରିସେଟ କରିବାକୁ ଆପଣଙ୍କ IT ଆଡମିନଙ୍କୁ କୁହନ୍ତୁ" - "ସ୍କ୍ରିନ୍‌ ଲକ୍‌ ବିକଳ୍ପ" + "ସ୍କ୍ରିନ ଲକ ବିକଳ୍ପ" "ସ୍କ୍ରିନ ଲକ ବିକଳ୍ପ" "ଅନଲକକୁ ସ୍ୱତଃ-ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ଯଦି ଆପଣ 6 କିମ୍ବା ଅଧିକ ଅଙ୍କର ଏକ ସଠିକ PIN ଇନପୁଟ କରନ୍ତି ତେବେ ସ୍ୱତଃ ଅନଲକ କରନ୍ତୁ। ସୁନିଶ୍ଚିତ କରିବା ପାଇଁ Enter ଟାପ କରିବାଠାରୁ ଏହା ସାମାନ୍ୟ କମ ସୁରକ୍ଷିତ ଅଟେ।" diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 7399defbea6..b379a9cdece 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -247,8 +247,8 @@ "Текст на заблокированном экране" "Нет текста" "Например, Android Саши" - "Местоположение" - "Определять местоположение" + "Геолокация" + "Включить геолокацию" "Откл." "{count,plural, =1{Включено. Доступ к данным о местоположении есть у # приложения.}one{Включено. Доступ к данным о местоположении есть у # приложения.}few{Включено. Доступ к данным о местоположении есть у # приложений.}many{Включено. Доступ к данным о местоположении есть у # приложений.}other{Включено. Доступ к данным о местоположении есть у # приложения.}}" "Загрузка…" @@ -1348,9 +1348,9 @@ "Вставьте SIM-карту и перезагрузите устройство" "Подключитесь к Интернету" "Последние запросы местоположения" - "Геоданные для рабочего профиля" + "Геолокация в рабочем профиле" "Доступ приложений к геоданным" - "Доступ к геоданным отключен" + "Геолокация отключена" "{count,plural, =1{# приложение из {total} имеет доступ к данным о местоположении}one{# приложение из {total} имеет доступ к данным о местоположении}few{# приложения из {total} имеют доступ к данным о местоположении}many{# приложений из {total} имеют доступ к данным о местоположении}other{# приложения из {total} имеют доступ к данным о местоположении}}" "Доступ за последнее время" "Показать все" @@ -1420,7 +1420,7 @@ "Введите PIN-код рабочего профиля" "Пароли не совпадают" "PIN-коды не совпадают" - "Начертите\\n графический\\n ключ ещё раз" + "Начертите ключ ещё раз" "Способ блокировки" "Пароль был установлен" "PIN-код был установлен" @@ -1468,7 +1468,7 @@ "По завершении отпустите палец." "Соедините не менее %d точек." "Графический ключ сохранен." - "Начертите ключ ещё раз." + "Подтвердите графический ключ." "Ваш новый ключ разблокировки." "Подтвердить" "Воспроизвести повторно" @@ -1794,7 +1794,7 @@ "Чтобы услышать описание элемента, нажмите на него" "Настройки субтитров" "О настройках субтитров" - "Подробнее о настройках субтитров…" + "Подробнее о настройках субтитров" "Увеличение" "Быстрый запуск увеличения" "Лупа при вводе текста" @@ -2138,7 +2138,7 @@ "Добавить принтеры" "Включено" "Отключено" - "Добавить службу" + "Добавить сервис" "Добавить принтер" "Поиск" "Поиск принтеров…" @@ -2182,8 +2182,8 @@ "Подробная история" "Расход заряда батареи" "Расход заряда за последние 24 часа" - "Данные об исп. с момента полной зарядки" - "Расход заряда батареи приложениями" + "Данные с момента полной зарядки" + "Расход заряда приложением" "Расход батареи" "Режим энергопотребления" "Пакеты" @@ -2283,7 +2283,7 @@ "Задать расписание" "Продление времени работы от батареи" "Отключать при полном заряде" - "Отключать, когда уровень заряда достигает %1$s" + "Отключать при %1$s" "Режим энергосбережения отключается, когда уровень заряда достигает %1$s" @@ -3008,7 +3008,7 @@ "Отключено" "Включено/%1$s" "Включено/%1$s и %2$s" - "Функцию пространственного звучания можно также включить для устройств Bluetooth." + "Пространственное звучание можно также включить для устройств Bluetooth." "Настройки подключенных устройств" "{count,plural, =0{Нет настроенных расписаний}=1{Настроено 1 расписание}one{Настроено # расписание}few{Настроено # расписания}many{Настроено # расписаний}other{Настроено # расписания}}" "Не беспокоить" @@ -3079,13 +3079,13 @@ "%1$s. %2$s." "Включено/%1$s" "Включено" - "Всегда спрашивать" + "Спрашивать каждый раз" "Пока вы не отключите" "{count,plural, =1{1 час}one{# час}few{# часа}many{# часов}other{# часа}}" "{count,plural, =1{1 минута}one{# минута}few{# минуты}many{# минут}other{# минуты}}" "{count,plural, =0{Выключено}=1{Выключено/Автоматическое включение по 1 расписанию}one{Выключено/Автоматическое включение по # расписанию}few{Выключено/Автоматическое включение по # расписаниям}many{Выключено/Автоматическое включение по # расписаниям}other{Выключено/Автоматическое включение по # расписания}}" "Исключения для режима \"Не беспокоить\"" - "Пользователи" + "Люди" "Приложения" "Будильники и другие звуки" "Расписания" @@ -3423,7 +3423,7 @@ "Разрешить приложениям переопределять \"Не беспокоить\"" "Приложения, для которых не действует режим \"Не беспокоить\"" "Другие приложения" - "Приложения не выбраны." + "Приложения не выбраны" "Режим \"Не беспокоить\" действует для всех приложений" "Добавить приложения" "Все уведомления" @@ -4696,8 +4696,8 @@ "Об уведомлениях со вспышкой" "Выключено" "Вкл. (вспышка камеры)" - "Вкл. (подсветка экрана)" - "Вкл. (вспышка камеры и подсветка экрана)" + "Включено (подсветка экрана)" + "Включено (вспышка камеры и подсветка экрана)" "Вспышка камеры или экран замигают, когда придет уведомление или зазвучит будильник." "Экран замигает, когда придет уведомление или зазвучит будильник." "Используйте уведомление с подсветкой осторожно, если вы чувствительны к свету." diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 5385ca1a3d8..90c85cc51d0 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -4574,7 +4574,7 @@ "Vse aplikacije" "Ne dovoli" "Izjemno širok pas (UWB)" - "Pomaga določiti relativni položaj naprav v bližini, ki imajo UWB." + "Pomaga določiti relativni položaj naprav v bližini, ki imajo UWB" "Če želite uporabljati UWB, izklopite način za letalo." "Na trenutni lokaciji UWB ni na voljo." "Dostop do fotoaparata" diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 631be7f30e8..992b5877b08 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -508,7 +508,7 @@ "Ако заборавите откључавање екрана, администратор не може да га ресетује." "Подесите засебно закључавање за пословне апликације" "Ако заборавите како да откључате, затражите од ИТ администратора да то ресетује" - "Опције закључавања екрана" + "Опције откључавања екрана" "Опције откључавања екрана" "Откључавање које се аутоматски потврђује" "Откључава се аутоматски ако унесете PIN са 6 или више цифара. То је мало мање поуздано него да додирнете Enter да бисте потврдили." diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml index 78833cd48e6..8c2a80c0245 100644 --- a/res/values-te/strings.xml +++ b/res/values-te/strings.xml @@ -349,7 +349,7 @@ "ఇది ఎలా పని చేస్తుంది" "\"వేలిముద్ర అన్‌లాక్\" అన్నది మీ వేలిముద్రకు సంబంధించి ఒక ప్రత్యేకమైన మోడల్‌ను క్రియేట్ చేస్తుంది. ఫోన్‌ను అన్‌లాక్‌ చేస్తోంది మీరేనని వెరిఫై చేయడానికి ఈ మోడల్‌ ఉపయోగించబడుతుంది. ఫోన్‌ను సెటప్ చేసే సమయంలో ఈ వేలిముద్ర మోడల్‌ను క్రియేట్ చేయడానికి, మీరు మీ వేలిముద్ర ఇమేజ్‌లను విభిన్న స్థానాల నుండి తీసుకుంటారు." "అది వారేనని వెరిఫై చేయడానికి, వేలిముద్ర అన్‌లాక్ మీ చిన్నారి వేలిముద్ర ప్రత్యేకమైన మోడల్‌ను క్రియేట్ చేస్తుంది. సెటప్ సమయంలో ఈ వేలిముద్ర మోడల్‌ను క్రియేట్ చేయడానికి, వారి వేలిముద్ర తాలూకు ఇమేజ్‌లను వారు వివిధ స్థానాల నుండి తీసుకుంటారు." - "ఉత్తమ ఫలితాల కోసం, Google చేత సర్టిఫైడ్ చేయబడి తయారు చేయబడిన స్క్రీన్ ప్రొటెక్టర్‌ను ఉపయోగించండి. ఇతర స్క్రీన్ ప్రొటెక్టర్‌లతో, మీ వేలిముద్ర పని చేయకపోవచ్చు." + "ఉత్తమ ఫలితాల కోసం, \'Google కోసం తయారు చేసినది\'గా సర్టిఫికెట్ పొందిన స్క్రీన్ ప్రొటెక్టర్‌ను ఉపయోగించండి. ఇతర స్క్రీన్ ప్రొటెక్టర్‌లతో, మీ వేలిముద్ర పని చేయకపోవచ్చు." "ఉత్తమ ఫలితాల కోసం, Google చేత సర్టిఫైడ్ చేయబడి తయారు చేయబడిన స్క్రీన్ ప్రొటెక్టర్‌ను ఉపయోగించండి. ఇతర స్క్రీన్ ప్రొటెక్టర్‌లతో, మీ పిల్లల వేలిముద్ర పని చేయకపోవచ్చు." "వాచ్ అన్‌లాక్" @@ -405,7 +405,7 @@ "మీ వేలిముద్రను ఎలా సెటప్ చేయాలి" "ఇది మీ ఫోన్ వెనుక భాగంలో ఉంది. మీ చూపుడు వేలిని ఉపయోగించండి." "మీ స్క్రీన్ మీద వేలిముద్ర సెన్సార్ ఉంది. తదుపరి స్క్రీన్‌లో మీ వేలిముద్రను క్యాప్చర్ చేస్తారు." - "మొదలెట్టు" + "స్టార్ట్‌" "సెన్సార్‌ను కనుగొనడానికి, స్క్రీన్ అంతటా వేలిని జరపండి. వేలిముద్ర సెన్సార్‌ను తాకి & నొక్కి ఉంచండి." "పరికరం మరియు వేలిముద్ర సెన్సార్ లొకేషన్‌తో చిత్రపటం" "పేరు" diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml index 11c221c56c1..5d154f9396c 100644 --- a/res/values-th/strings.xml +++ b/res/values-th/strings.xml @@ -404,7 +404,7 @@ "แตะปุ่มเปิด/ปิดโดยไม่ต้องกด" "วิธีตั้งค่าลายนิ้วมือ" "เซ็นเซอร์อยู่ที่ด้านหลังโทรศัพท์ โปรดใช้นิ้วขี้" - "เซ็นเซอร์ลายนิ้วมืออยู่ในหน้าจอ คุณจะบันทึกลายนิ้วมือในหน้าจอถัดไป" + "เซ็นเซอร์ลายนิ้วมืออยู่บนหน้าจอ คุณจะบันทึกลายนิ้วมือในหน้าจอถัดไป" "เริ่ม" "เลื่อนนิ้วไปมาบนหน้าจอเพื่อค้นหาเซ็นเซอร์ แตะเซ็นเซอร์ลายนิ้วมือค้างไว้" "ภาพประกอบที่มีตำแหน่งของอุปกรณ์และเซ็นเซอร์ลายนิ้วมือ" diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 4e7cf868a56..06b6f612212 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -349,8 +349,8 @@ "Як це працює" "Функція розблокування відбитком пальця створює унікальну модель вашого відбитка, щоб підтверджувати вашу особу. Для цього під час налаштування потрібно створити зображення свого відбитка пальця в різних положеннях." "Функція розблокування відбитком пальця створює унікальну модель відбитка вашої дитини, щоб підтверджувати її особу. Для цього дитині потрібно зберегти зображення свого відбитка пальця в різних положеннях." - "Рекомендуємо використовувати захисну плівку чи скло, що мають позначку Made for Google. Якщо вони не матимуть такої позначки, відбиток пальця може не розпізнаватися." - "Рекомендуємо використовувати захисну плівку чи скло, що мають позначку \"Створено для Google\". Якщо вони не матимуть такої позначки, відбиток пальця дитини може не розпізнаватися." + "Рекомендуємо використовувати захисну плівку чи скло, що мають позначку Made for Google. Пристрої з іншими захисними плівками чи склом можуть не розпізнавати відбиток пальця." + "Рекомендуємо використовувати захисну плівку чи скло, що мають позначку Made for Google. Пристрої з іншими захисними плівками чи склом можуть не розпізнавати відбиток пальця." "Розблокування годинником" "Якщо ви налаштуєте розблокування за допомогою фейс-контролю й відбитка пальця, телефон запитуватиме ваш відбиток пальця, коли ви в масці або навколо вас темно.\n\nЯкщо не вдається розпізнати ваше обличчя або відбиток пальця, ви можете розблокувати пристрій за допомогою годинника." diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 51d1da063d2..77167e4e05d 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -508,8 +508,8 @@ "Quản trị viên CNTT sẽ không thể đặt lại nếu bạn quên phương thức khóa màn hình." "Đặt khóa riêng cho công việc" "Nếu bạn quên kiểu khóa này, hãy yêu cầu quản trị viên CNTT đặt lại" - "Tùy chọn phương thức khóa màn hình" - "Tùy chọn phương thức khóa màn hình" + "Các phương thức khóa màn hình" + "Các phương thức khóa màn hình" "Mở khoá bằng cách tự động xác nhận" "Tự động mở khoá nếu bạn nếu nhập đúng mã PIN gồm từ 6 chữ số trở lên. Cách này sẽ kém bảo mật hơn một chút so với việc nhấn Enter để xác nhận." "Tự động xác nhận bằng mã PIN chính xác" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index f7b90636994..a36fc2f5bcd 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -508,7 +508,7 @@ "如果您忘记屏幕解锁方式,IT 管理员将无法为您重置。" "为工作资料设置单独的屏幕锁定方式" "如果忘记了此锁定方式,请让您的 IT 管理员重置" - "屏幕锁定选项" + "屏幕解锁方式" "解锁方式" "自动确认解锁" "如果输入的 PIN 码(6 位以上)正确,设备会自动解锁。与通过点按 Enter 键确认 PIN 相比,前者的安全性略低。" From b091995671839487c7d8630f42c7d5556b8a9585 Mon Sep 17 00:00:00 2001 From: tomhsu Date: Fri, 12 Jul 2024 03:59:19 +0000 Subject: [PATCH 06/10] Change API requestIsEnabled to requestIsSessionStarted - Avoid using APM/WIFI/BT in settings app from any transitioning from TN to NTN. Flag: EXEMPT bugfix fix: 354806125 Test: Manual test Change-Id: Id65567971b3d1ea3e177a0cc1e1304446d8fa0e8 --- src/com/android/settings/bluetooth/BluetoothEnabler.java | 2 +- .../settings/network/AirplaneModePreferenceController.java | 3 ++- src/com/android/settings/wifi/slice/WifiSlice.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java index df5cc72fae1..a5d0bc67769 100644 --- a/src/com/android/settings/bluetooth/BluetoothEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -132,7 +132,7 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh new Thread(() -> { try { - mIsSatelliteOn.set(mSatelliteRepository.requestIsEnabled( + mIsSatelliteOn.set(mSatelliteRepository.requestIsSessionStarted( Executors.newSingleThreadExecutor()).get(3000, TimeUnit.MILLISECONDS)); } catch (InterruptedException | ExecutionException | TimeoutException e) { Log.e(TAG, "Error to get satellite status : " + e); diff --git a/src/com/android/settings/network/AirplaneModePreferenceController.java b/src/com/android/settings/network/AirplaneModePreferenceController.java index b1f6e5052ff..d4bd4a369c0 100644 --- a/src/com/android/settings/network/AirplaneModePreferenceController.java +++ b/src/com/android/settings/network/AirplaneModePreferenceController.java @@ -162,7 +162,8 @@ public class AirplaneModePreferenceController extends TogglePreferenceController public void onResume() { try { mIsSatelliteOn.set( - mSatelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor()) + mSatelliteRepository + .requestIsSessionStarted(Executors.newSingleThreadExecutor()) .get(2000, TimeUnit.MILLISECONDS)); } catch (ExecutionException | TimeoutException | InterruptedException e) { Log.e(TAG, "Error to get satellite status : " + e); diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java index ff448a86692..3bb50d35817 100644 --- a/src/com/android/settings/wifi/slice/WifiSlice.java +++ b/src/com/android/settings/wifi/slice/WifiSlice.java @@ -431,7 +431,7 @@ public class WifiSlice implements CustomSliceable { boolean isSatelliteOn = false; try { isSatelliteOn = - satelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor()) + satelliteRepository.requestIsSessionStarted(Executors.newSingleThreadExecutor()) .get(2000, TimeUnit.MILLISECONDS); } catch (ExecutionException | TimeoutException | InterruptedException e) { Log.e(TAG, "Error to get satellite status : " + e); From a9eb7c90dd9997d89aa45db8445aca15ae1bfd4a Mon Sep 17 00:00:00 2001 From: pajacechen Date: Tue, 30 Jul 2024 10:05:03 +0800 Subject: [PATCH 07/10] [Bugfix] Fix the failed test case Fix the failed test case that missed from ag/28495857 Test: Unit Test Fix: 348563863 Fix: 356152111 Flag: EXEMPT bug fix Change-Id: I852da018c6967c6ca13e3aea72290a2d6be8bfdd --- .../src/com/android/settings/fuelgauge/BatteryInfoTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index 851dc79a03f..8d6cc08597f 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -75,7 +75,8 @@ public class BatteryInfoTest { private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full"; private static final String STATUS_NOT_CHARGING = "Not charging"; private static final String STATUS_CHARGING_FUTURE_BYPASS = "50% - Charging"; - private static final String STATUS_CHARGING_PAUSED = "50% - Charging optimized"; + private static final String STATUS_CHARGING_PAUSED = + "50% - Charging on hold to protect battery"; private static final long REMAINING_TIME_NULL = -1; private static final long REMAINING_TIME = 2; // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml From cc2114a797f15fd1ac0120100d7b5d70e3ffc500 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Mon, 29 Jul 2024 13:31:53 +0800 Subject: [PATCH 08/10] Add multi-toggle preference UI for device details page BUG: 343317785 Test: atest DeviceSettingRepositoryTest Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I67e7647fee39e789cc1342943f69e7ddc170d0eb --- res/drawable/ic_close.xml | 24 ++ .../ui/MultiTogglePreferenceGroup.kt | 293 ++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 res/drawable/ic_close.xml create mode 100644 src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml new file mode 100644 index 00000000000..de2085ce7e2 --- /dev/null +++ b/res/drawable/ic_close.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt new file mode 100644 index 00000000000..e4ca00d47a9 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties +import com.android.settings.R +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.dialog.getDialogWidth + +@Composable +fun MultiTogglePreferenceGroup( + preferenceModels: List, +) { + var settingIdForPopUp by remember { mutableStateOf(null) } + + settingIdForPopUp?.let { id -> + preferenceModels.find { it.id == id }?.let { dialog(it) { settingIdForPopUp = null } } + } + + Row( + modifier = Modifier.padding(SettingsDimension.itemPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + preferenceModels.forEach { preferenceModel -> + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row { + Surface( + modifier = Modifier.height(64.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surface + ) { + Button( + modifier = + Modifier.fillMaxSize().padding(8.dp).semantics { + role = Role.Switch + toggleableState = + if (preferenceModel.isActive) { + ToggleableState.On + } else { + ToggleableState.Off + } + contentDescription = preferenceModel.title + }, + onClick = { settingIdForPopUp = preferenceModel.id }, + shape = RoundedCornerShape(20.dp), + colors = getButtonColors(preferenceModel.isActive), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + preferenceModel.toggles[preferenceModel.state.selectedIndex] + .icon + .asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = LocalContentColor.current + ) + } + } + } + Row { Text(text = preferenceModel.title, fontSize = 12.sp) } + } + } + } +} + +@Composable +private fun getButtonColors(isActive: Boolean) = + if (isActive) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ) + } else { + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun dialog( + multiTogglePreference: DeviceSettingModel.MultiTogglePreference, + onDismiss: () -> Unit +) { + BasicAlertDialog( + onDismissRequest = { onDismiss() }, + modifier = Modifier.width(getDialogWidth()), + properties = DialogProperties(usePlatformDefaultWidth = false), + content = { + Card( + shape = RoundedCornerShape(28.dp), + modifier = Modifier.fillMaxWidth().height(192.dp), + content = { + Box { + Button( + onClick = { onDismiss() }, + modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp), + contentPadding = PaddingValues(12.dp), + colors = + ButtonDefaults.buttonColors(containerColor = Color.Transparent), + ) { + Icon( + painterResource(id = R.drawable.ic_close), + null, + tint = MaterialTheme.colorScheme.inverseSurface + ) + } + Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) { + dialogContent(multiTogglePreference) + } + } + }, + ) + } + ) +} + +@Composable +private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiTogglePreference) { + Column { + Row( + modifier = Modifier.fillMaxWidth().height(24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Text(text = multiTogglePreference.title, fontSize = 16.sp) + } + Spacer(modifier = Modifier.height(20.dp)) + var selectedRect by remember { mutableStateOf(null) } + val offset = + selectedRect?.let { rect -> + animateFloatAsState(targetValue = rect.left, finishedListener = {}).value + } + + Row( + modifier = + Modifier.fillMaxWidth() + .height(64.dp) + .background( + MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(28.dp) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Box { + offset?.let { offset -> + with(LocalDensity.current) { + Box( + modifier = + Modifier.offset(offset.toDp(), 0.dp) + .height(selectedRect!!.height.toDp()) + .width(selectedRect!!.width.toDp()) + .background( + MaterialTheme.colorScheme.tertiaryContainer, + shape = RoundedCornerShape(20.dp) + ) + ) + } + } + Row { + for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) { + val selected = idx == multiTogglePreference.state.selectedIndex + Column( + modifier = + Modifier.weight(1f) + .padding(horizontal = 8.dp) + .height(48.dp) + .background( + Color.Transparent, + shape = RoundedCornerShape(28.dp) + ) + .onGloballyPositioned { layoutCoordinates -> + if (selected) { + selectedRect = layoutCoordinates.boundsInParent() + } + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button( + onClick = { + multiTogglePreference.updateState( + DeviceSettingStateModel.MultiTogglePreferenceState(idx) + ) + }, + modifier = Modifier.fillMaxSize(), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = LocalContentColor.current + ), + ) { + Icon( + bitmap = toggle.icon.asImageBitmap(), + null, + modifier = Modifier.size(24.dp), + tint = LocalContentColor.current + ) + } + } + } + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth().height(32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + for (toggle in multiTogglePreference.toggles) { + Text( + text = toggle.label, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f).padding(horizontal = 8.dp) + ) + } + } + } +} From 0be6705da6e53efb6a74a9433de656ab20a435a6 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 30 Jul 2024 13:45:54 +0800 Subject: [PATCH 09/10] Call wifiStatusTracker.fetchInitialState first Before set listening to true, otherwise could cause race condition. Fix: 354500692 Flag: EXEMPT bug fix Test: manual - check wifi summary Change-Id: I4160f89fae666ac02b816b7d9a69bac581bbd29e --- .../settings/wifi/repository/WifiStatusRepository.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/wifi/repository/WifiStatusRepository.kt b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt index f97ed492507..fe4ba6c9035 100644 --- a/src/com/android/settings/wifi/repository/WifiStatusRepository.kt +++ b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt @@ -50,14 +50,20 @@ class WifiStatusRepository( var wifiStatusTracker: WifiStatusTracker? = null wifiStatusTracker = wifiStatusTrackerFactory { wifiStatusTracker?.let(::trySend) } + // Fetches initial state first, before set listening to true, otherwise could cause + // race condition. + wifiStatusTracker.fetchInitialState() + trySend(wifiStatusTracker) + context .broadcastReceiverFlow(INTENT_FILTER) - .onEach { intent -> wifiStatusTracker.handleBroadcast(intent) } + .onEach { intent -> + wifiStatusTracker.handleBroadcast(intent) + trySend(wifiStatusTracker) + } .launchIn(this) wifiStatusTracker.setListening(true) - wifiStatusTracker.fetchInitialState() - trySend(wifiStatusTracker) awaitClose { wifiStatusTracker.setListening(false) } } From 413e85fa7a9d8d1352cd9864888678b5c6733e89 Mon Sep 17 00:00:00 2001 From: "Chaitanya Cheemala (xWF)" Date: Tue, 30 Jul 2024 09:31:35 +0000 Subject: [PATCH 10/10] Revert "Change API requestIsEnabled to requestIsSessionStarted" Revert submission 28533501-NTN_check_modem_state Reason for revert: Likely culprit for b/356320274 - verifying through ABTD before revert submission. This is part of the standard investigation process, and does not mean your CL will be reverted. Reverted changes: /q/submissionid:28533501-NTN_check_modem_state Change-Id: I4cdb936eb6a27eeee0aba6a5adfcf9cbdfacd5eb --- src/com/android/settings/bluetooth/BluetoothEnabler.java | 2 +- .../settings/network/AirplaneModePreferenceController.java | 3 +-- src/com/android/settings/wifi/slice/WifiSlice.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java index a5d0bc67769..df5cc72fae1 100644 --- a/src/com/android/settings/bluetooth/BluetoothEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -132,7 +132,7 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh new Thread(() -> { try { - mIsSatelliteOn.set(mSatelliteRepository.requestIsSessionStarted( + mIsSatelliteOn.set(mSatelliteRepository.requestIsEnabled( Executors.newSingleThreadExecutor()).get(3000, TimeUnit.MILLISECONDS)); } catch (InterruptedException | ExecutionException | TimeoutException e) { Log.e(TAG, "Error to get satellite status : " + e); diff --git a/src/com/android/settings/network/AirplaneModePreferenceController.java b/src/com/android/settings/network/AirplaneModePreferenceController.java index d4bd4a369c0..b1f6e5052ff 100644 --- a/src/com/android/settings/network/AirplaneModePreferenceController.java +++ b/src/com/android/settings/network/AirplaneModePreferenceController.java @@ -162,8 +162,7 @@ public class AirplaneModePreferenceController extends TogglePreferenceController public void onResume() { try { mIsSatelliteOn.set( - mSatelliteRepository - .requestIsSessionStarted(Executors.newSingleThreadExecutor()) + mSatelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor()) .get(2000, TimeUnit.MILLISECONDS)); } catch (ExecutionException | TimeoutException | InterruptedException e) { Log.e(TAG, "Error to get satellite status : " + e); diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java index 3bb50d35817..ff448a86692 100644 --- a/src/com/android/settings/wifi/slice/WifiSlice.java +++ b/src/com/android/settings/wifi/slice/WifiSlice.java @@ -431,7 +431,7 @@ public class WifiSlice implements CustomSliceable { boolean isSatelliteOn = false; try { isSatelliteOn = - satelliteRepository.requestIsSessionStarted(Executors.newSingleThreadExecutor()) + satelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor()) .get(2000, TimeUnit.MILLISECONDS); } catch (ExecutionException | TimeoutException | InterruptedException e) { Log.e(TAG, "Error to get satellite status : " + e);