diff --git a/res/layout/settings_panel.xml b/res/layout/settings_panel.xml index aec898c99c6..3405ef0d844 100644 --- a/res/layout/settings_panel.xml +++ b/res/layout/settings_panel.xml @@ -16,4 +16,5 @@ \ No newline at end of file + android:layout_width="match_parent" + android:animateLayoutChanges="true"/> \ No newline at end of file diff --git a/src/com/android/settings/nfc/NfcPreferenceController.java b/src/com/android/settings/nfc/NfcPreferenceController.java index 04f288d721b..2ca3b23daea 100644 --- a/src/com/android/settings/nfc/NfcPreferenceController.java +++ b/src/com/android/settings/nfc/NfcPreferenceController.java @@ -15,20 +15,26 @@ */ package com.android.settings.nfc; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; +import android.net.Uri; import android.nfc.NfcAdapter; import android.provider.Settings; - +import android.util.Log; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import androidx.preference.SwitchPreference; import com.android.settings.core.TogglePreferenceController; +import com.android.settings.slices.SliceBackgroundWorker; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; +import java.io.IOException; + public class NfcPreferenceController extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause { @@ -51,8 +57,7 @@ public class NfcPreferenceController extends TogglePreferenceController return; } - final SwitchPreference switchPreference = - (SwitchPreference) screen.findPreference(getPreferenceKey()); + final SwitchPreference switchPreference = screen.findPreference(getPreferenceKey()); mNfcEnabler = new NfcEnabler(mContext, switchPreference); @@ -86,14 +91,6 @@ public class NfcPreferenceController extends TogglePreferenceController : UNSUPPORTED_ON_DEVICE; } - @Override - public IntentFilter getIntentFilter() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); - filter.addAction(NfcAdapter.EXTRA_ADAPTER_STATE); - return filter; - } - @Override public boolean hasAsyncUpdate() { return true; @@ -104,6 +101,11 @@ public class NfcPreferenceController extends TogglePreferenceController return true; } + @Override + public Class getBackgroundWorkerClass() { + return NfcSliceWorker.class; + } + @Override public void onResume() { if (mAirplaneModeObserver != null) { @@ -135,4 +137,77 @@ public class NfcPreferenceController extends TogglePreferenceController Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); return toggleable != null && toggleable.contains(Settings.Global.RADIO_NFC); } + + /** + * Listener for background changes to NFC. + * + *

+ * Listen to broadcasts from {@link NfcAdapter}. The worker will call notify changed on the + * NFC Slice only when the following extras are present in the broadcast: + *

+ */ + public static class NfcSliceWorker extends SliceBackgroundWorker { + + private static final String TAG = "NfcSliceWorker"; + + private static final IntentFilter NFC_FILTER = + new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + + private NfcUpdateReceiver mUpdateObserver; + + public NfcSliceWorker(Context context, Uri uri) { + super(context, uri); + mUpdateObserver = new NfcUpdateReceiver(this); + } + + @Override + protected void onSlicePinned() { + getContext().registerReceiver(mUpdateObserver, NFC_FILTER); + } + + @Override + protected void onSliceUnpinned() { + getContext().unregisterReceiver(mUpdateObserver); + } + + @Override + public void close() throws IOException { + mUpdateObserver = null; + } + + public void updateSlice() { + notifySliceChange(); + } + + public class NfcUpdateReceiver extends BroadcastReceiver { + + private final int NO_EXTRA = -1; + + private final NfcSliceWorker mSliceBackgroundWorker; + + public NfcUpdateReceiver(NfcSliceWorker sliceWorker) { + mSliceBackgroundWorker = sliceWorker; + } + + @Override + public void onReceive(Context context, Intent intent) { + final int nfcStateExtra = intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, + NO_EXTRA); + + // Do nothing if state change is empty, or an intermediate step. + if ( (nfcStateExtra == NO_EXTRA) + || (nfcStateExtra == NfcAdapter.STATE_TURNING_ON) + || (nfcStateExtra == NfcAdapter.STATE_TURNING_OFF)) { + Log.d(TAG, "Transitional update, dropping broadcast"); + return; + } + + Log.d(TAG, "Nfc broadcast received, updating Slice."); + mSliceBackgroundWorker.updateSlice(); + } + } + } } diff --git a/src/com/android/settings/slices/CustomSliceable.java b/src/com/android/settings/slices/CustomSliceable.java index b48f22a5237..0b97bedc54f 100644 --- a/src/com/android/settings/slices/CustomSliceable.java +++ b/src/com/android/settings/slices/CustomSliceable.java @@ -83,18 +83,6 @@ public interface CustomSliceable extends Sliceable { */ Intent getIntent(); - /** - * Settings Slices which require background work, such as updating lists should implement a - * {@link SliceBackgroundWorker} and return it here. An example of background work is updating - * a list of Wifi networks available in the area. - * - * @return a {@link Class} to perform background work for the - * slice. - */ - default Class getBackgroundWorkerClass() { - return null; - } - /** * Standardize the intents returned to indicate actions by the Slice. *

diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 5c662e5c7e0..397b2fc631c 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -153,7 +153,7 @@ public class SettingsSliceProvider extends SliceProvider { if (filter != null) { registerIntentToUri(filter, sliceUri); } - ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable)); + ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable, sliceUri)); return; } @@ -326,20 +326,19 @@ public class SettingsSliceProvider extends SliceProvider { } } - private void startBackgroundWorker(CustomSliceable sliceable) { + private void startBackgroundWorker(Sliceable sliceable, Uri uri) { final Class workerClass = sliceable.getBackgroundWorkerClass(); if (workerClass == null) { return; } - final Uri uri = sliceable.getUri(); if (mPinnedWorkers.containsKey(uri)) { return; } Log.d(TAG, "Starting background worker for: " + uri); final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance( - getContext(), sliceable); + getContext(), sliceable, uri); mPinnedWorkers.put(uri, worker); worker.onSlicePinned(); } @@ -397,6 +396,8 @@ public class SettingsSliceProvider extends SliceProvider { registerIntentToUri(filter, uri); } + ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri)); + final List pinnedSlices = getContext().getSystemService( SliceManager.class).getPinnedSlices(); if (pinnedSlices.contains(uri)) { diff --git a/src/com/android/settings/slices/SliceBackgroundWorker.java b/src/com/android/settings/slices/SliceBackgroundWorker.java index 995394e7d41..559aa711e2a 100644 --- a/src/com/android/settings/slices/SliceBackgroundWorker.java +++ b/src/com/android/settings/slices/SliceBackgroundWorker.java @@ -80,13 +80,12 @@ public abstract class SliceBackgroundWorker implements Closeable { * Returns the singleton instance of the {@link SliceBackgroundWorker} for specified {@link * CustomSliceable} */ - static SliceBackgroundWorker getInstance(Context context, CustomSliceable sliceable) { - final Uri uri = sliceable.getUri(); + static SliceBackgroundWorker getInstance(Context context, Sliceable sliceable, Uri uri) { SliceBackgroundWorker worker = getInstance(uri); if (worker == null) { final Class workerClass = sliceable.getBackgroundWorkerClass(); - worker = createInstance(context, uri, workerClass); + worker = createInstance(context.getApplicationContext(), uri, workerClass); LIVE_WORKERS.put(uri, worker); } return worker; diff --git a/src/com/android/settings/slices/Sliceable.java b/src/com/android/settings/slices/Sliceable.java index b00ab8207eb..c1661f8392a 100644 --- a/src/com/android/settings/slices/Sliceable.java +++ b/src/com/android/settings/slices/Sliceable.java @@ -91,4 +91,16 @@ public interface Sliceable { final String toast = context.getString(R.string.copyable_slice_toast, messageTitle); Toast.makeText(context, toast, Toast.LENGTH_SHORT).show(); } + + /** + * Settings Slices which require background work, such as updating lists should implement a + * {@link SliceBackgroundWorker} and return it here. An example of background work is updating + * a list of Wifi networks available in the area. + * + * @return a {@link Class} to perform background work for the + * slice. + */ + default Class getBackgroundWorkerClass() { + return null; + } } diff --git a/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java index 8883ddf7e3a..e3672c92dc9 100644 --- a/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java @@ -19,10 +19,13 @@ package com.android.settings.nfc; import static com.google.common.truth.Truth.assertThat; 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 android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.nfc.NfcAdapter; import android.nfc.NfcManager; import android.os.UserManager; @@ -31,6 +34,10 @@ import android.provider.Settings; import androidx.preference.PreferenceScreen; import androidx.preference.SwitchPreference; +import com.android.settings.nfc.NfcPreferenceController.NfcSliceWorker; +import com.android.settings.nfc.NfcPreferenceController.NfcSliceWorker.NfcUpdateReceiver; +import com.android.settings.slices.SliceBuilderUtils; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -246,4 +253,67 @@ public class NfcPreferenceControllerTest { assertThat(mNfcController.mAirplaneModeObserver).isNull(); } + + @Test + public void ncfSliceWorker_nfcBroadcast_noExtra_sliceDoesntUpdate() { + final NfcSliceWorker worker = spy(new NfcSliceWorker(mContext, getDummyUri())); + final NfcUpdateReceiver receiver = worker.new NfcUpdateReceiver(worker); + final Intent triggerIntent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + + receiver.onReceive(mContext, triggerIntent); + + verify(worker, times(0)).updateSlice(); + } + + @Test + public void ncfSliceWorker_nfcBroadcast_turningOn_sliceDoesntUpdate() { + final NfcSliceWorker worker = spy(new NfcSliceWorker(mContext, getDummyUri())); + final NfcUpdateReceiver receiver = worker.new NfcUpdateReceiver(worker); + final Intent triggerIntent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + triggerIntent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_TURNING_ON); + + receiver.onReceive(mContext, triggerIntent); + + verify(worker, times(0)).updateSlice(); + } + + @Test + public void ncfSliceWorker_nfcBroadcast_turningOff_sliceDoesntUpdate() { + final NfcSliceWorker worker = spy(new NfcSliceWorker(mContext, getDummyUri())); + final NfcUpdateReceiver receiver = worker.new NfcUpdateReceiver(worker); + final Intent triggerIntent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + triggerIntent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_TURNING_OFF); + + receiver.onReceive(mContext, triggerIntent); + + verify(worker, times(0)).updateSlice(); + } + + @Test + public void ncfSliceWorker_nfcBroadcast_nfcOn_sliceUpdates() { + final NfcSliceWorker worker = spy(new NfcSliceWorker(mContext, getDummyUri())); + final NfcUpdateReceiver receiver = worker.new NfcUpdateReceiver(worker); + final Intent triggerIntent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + triggerIntent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_ON); + + receiver.onReceive(mContext, triggerIntent); + + verify(worker).updateSlice(); + } + + @Test + public void ncfSliceWorker_nfcBroadcast_nfcOff_sliceUpdates() { + final NfcSliceWorker worker = spy(new NfcSliceWorker(mContext, getDummyUri())); + final NfcUpdateReceiver receiver = worker.new NfcUpdateReceiver(worker); + final Intent triggerIntent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + triggerIntent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_OFF); + + receiver.onReceive(mContext, triggerIntent); + + verify(worker).updateSlice(); + } + + private Uri getDummyUri() { + return SliceBuilderUtils.getUri("action/nfc", false); + } } diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java index 726e4bdf300..a693f349894 100644 --- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -64,6 +64,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -183,6 +184,20 @@ public class SettingsSliceProviderTest { verify(mProvider).registerIntentToUri(eq(FakeToggleController.INTENT_FILTER), eq(uri)); } + @Test + public void loadSlice_registersBackgroundListener() { + insertSpecialCase(KEY); + final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); + + mProvider.loadSlice(uri); + + Robolectric.flushForegroundThreadScheduler(); + Robolectric.flushBackgroundThreadScheduler(); + + assertThat(mProvider.mPinnedWorkers.get(uri).getClass()) + .isEqualTo(FakeToggleController.TestWorker.class); + } + @Test public void testLoadSlice_doesNotCacheWithoutPin() { insertSpecialCase(KEY); diff --git a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java index d1677cd7382..e7854874c54 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java @@ -19,10 +19,14 @@ package com.android.settings.testutils; import android.content.Context; import android.content.IntentFilter; +import android.net.Uri; import android.net.wifi.WifiManager; import android.provider.Settings; import com.android.settings.core.TogglePreferenceController; +import com.android.settings.slices.SliceBackgroundWorker; + +import java.io.IOException; public class FakeToggleController extends TogglePreferenceController { @@ -70,6 +74,11 @@ public class FakeToggleController extends TogglePreferenceController { return true; } + @Override + public Class getBackgroundWorkerClass() { + return TestWorker.class; + } + @Override public boolean hasAsyncUpdate() { return mIsAsyncUpdate; @@ -78,4 +87,23 @@ public class FakeToggleController extends TogglePreferenceController { public void setAsyncUpdate(boolean isAsyncUpdate) { mIsAsyncUpdate = isAsyncUpdate; } + + public static class TestWorker extends SliceBackgroundWorker { + + public TestWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + } + + @Override + protected void onSliceUnpinned() { + } + + @Override + public void close() throws IOException { + } + } }