}
/**
+ * Settings Slices which can represent component lists that are updatable by the
+ * {@link SliceBackgroundWorker} returned here.
+ *
+ * @return a {@link SliceBackgroundWorker} for fetching the list of results in the background.
+ */
+ default SliceBackgroundWorker getBackgroundWorker() {
+ return null;
+ }
+
+ /**
* Standardize the intents returned to indicate actions by the Slice.
* <p>
* The {@link PendingIntent} is linked to {@link SliceBroadcastReceiver} where the Intent
import android.provider.Settings;
import android.provider.SettingsSlicesContract;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Log;
final Set<Uri> mRegisteredUris = new ArraySet<>();
+ final Map<Uri, SliceBackgroundWorker> mWorkerMap = new ArrayMap<>();
+
public SettingsSliceProvider() {
super(READ_SEARCH_INDEXABLES);
}
if (filter != null) {
registerIntentToUri(filter, sliceUri);
}
+ startBackgroundWorker(sliceable);
return;
}
SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri);
mRegisteredUris.remove(sliceUri);
}
+ stopBackgroundWorker(sliceUri);
mSliceDataCache.remove(sliceUri);
}
}
}
+ private void startBackgroundWorker(CustomSliceable sliceable) {
+ final SliceBackgroundWorker worker = sliceable.getBackgroundWorker();
+ if (worker == null) {
+ return;
+ }
+
+ final Uri uri = sliceable.getUri();
+ Log.d(TAG, "Starting background worker for: " + uri);
+ if (mWorkerMap.containsKey(uri)) {
+ return;
+ }
+
+ mWorkerMap.put(uri, worker);
+ worker.onSlicePinned();
+ }
+
+ private void stopBackgroundWorker(Uri uri) {
+ final SliceBackgroundWorker worker = mWorkerMap.get(uri);
+ if (worker != null) {
+ Log.d(TAG, "Stopping background worker for: " + uri);
+ worker.onSliceUnpinned();
+ mWorkerMap.remove(uri);
+ }
+ }
+
private List<Uri> buildUrisFromKeys(List<String> keys, String authority) {
final List<Uri> descendants = new ArrayList<>();
--- /dev/null
+/*
+ * Copyright (C) 2018 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.slices;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Slice background worker is used to make Settings Slices be able to work with data that is
+ * changing continuously, e.g. available Wi-Fi networks.
+ *
+ * The background worker will be started at {@link SettingsSliceProvider#onSlicePinned(Uri)}, and be
+ * stopped at {@link SettingsSliceProvider#onSliceUnpinned(Uri)}.
+ *
+ * {@link SliceBackgroundWorker} caches the results, uses the cache to compare if there is any data
+ * changed, and then notifies the Slice {@link Uri} to update.
+ */
+public abstract class SliceBackgroundWorker<E> {
+
+ private final ContentResolver mContentResolver;
+ private final Uri mUri;
+
+ private List<E> mCachedResults;
+
+ protected SliceBackgroundWorker(ContentResolver cr, Uri uri) {
+ mContentResolver = cr;
+ mUri = uri;
+ }
+
+ /**
+ * Called when the Slice is pinned. This is the place to register callbacks or initialize scan
+ * tasks.
+ */
+ protected abstract void onSlicePinned();
+
+ /**
+ * Called when the Slice is unpinned. This is the place to unregister callbacks or perform any
+ * final cleanup.
+ */
+ protected abstract void onSliceUnpinned();
+
+ /**
+ * @return a {@link List} of cached results
+ */
+ public final List<E> getResults() {
+ return mCachedResults == null ? null : new ArrayList<>(mCachedResults);
+ }
+
+ /**
+ * Update the results when data changes
+ */
+ protected final void updateResults(List<E> results) {
+ boolean needNotify = false;
+
+ if (results == null) {
+ if (mCachedResults != null) {
+ needNotify = true;
+ }
+ } else {
+ needNotify = !results.equals(mCachedResults);
+ }
+
+ if (needNotify) {
+ mCachedResults = results;
+ mContentResolver.notifyChange(mUri, null);
+ }
+ }
+}
private static final String TAG = "WifiDialogActivity";
- private static final int RESULT_CONNECTED = RESULT_FIRST_USER;
- private static final int RESULT_FORGET = RESULT_FIRST_USER + 1;
-
- private static final String KEY_ACCESS_POINT_STATE = "access_point_state";
- private static final String KEY_WIFI_CONFIGURATION = "wifi_configuration";
-
/**
* Boolean extra indicating whether this activity should connect to an access point on the
* caller's behalf. If this is set to false, the caller should check
*/
@VisibleForTesting
static final String KEY_CONNECT_FOR_CALLER = "connect_for_caller";
+ static final String KEY_ACCESS_POINT_STATE = "access_point_state";
+ private static final String KEY_WIFI_CONFIGURATION = "wifi_configuration";
+
+ private static final int RESULT_CONNECTED = RESULT_FIRST_USER;
+ private static final int RESULT_FORGET = RESULT_FIRST_USER + 1;
private WifiDialog mDialog;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.provider.SettingsSlicesContract;
import android.text.TextUtils;
import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.Utils;
+import com.android.settings.core.SubSettingLauncher;
import com.android.settings.slices.CustomSliceable;
+import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.slices.SliceBuilderUtils;
+import com.android.settings.wifi.details.WifiNetworkDetailsFragment;
+import com.android.settingslib.wifi.AccessPoint;
+import com.android.settingslib.wifi.WifiTracker;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Utility class to build a Wifi Slice, and handle all associated actions.
final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */,
isWifiEnabled);
- return new ListBuilder(mContext, WIFI_URI, ListBuilder.INFINITY)
+ final ListBuilder listBuilder = new ListBuilder(mContext, WIFI_URI, ListBuilder.INFINITY)
.setAccentColor(color)
.addRow(new RowBuilder()
.setTitle(title)
.setSubtitle(summary)
.addEndItem(toggleSliceAction)
- .setPrimaryAction(primarySliceAction))
- .build();
+ .setPrimaryAction(primarySliceAction));
+
+ if (isWifiEnabled) {
+ final List<AccessPoint> result = getBackgroundWorker().getResults();
+ if (result != null && !result.isEmpty()) {
+ for (AccessPoint ap : result) {
+ listBuilder.addRow(getAccessPointRow(ap));
+ }
+ listBuilder.setSeeMoreAction(primaryAction);
+ }
+ }
+ return listBuilder.build();
+ }
+
+ private RowBuilder getAccessPointRow(AccessPoint accessPoint) {
+ final String title = accessPoint.getConfigName();
+ final IconCompat levelIcon = IconCompat.createWithResource(mContext,
+ com.android.settingslib.Utils.getWifiIconResource(accessPoint.getLevel()));
+ final RowBuilder rowBuilder = new RowBuilder()
+ .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE)
+ .setTitle(title)
+ .setSubtitle(accessPoint.getSettingsSummary())
+ .setPrimaryAction(new SliceAction(
+ getAccessPointAction(accessPoint), levelIcon, title));
+
+ final IconCompat endIcon = getEndIcon(accessPoint);
+ if (endIcon != null) {
+ rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE);
+ }
+ return rowBuilder;
+ }
+
+ private IconCompat getEndIcon(AccessPoint accessPoint) {
+ if (accessPoint.isActive()) {
+ return IconCompat.createWithResource(mContext, R.drawable.ic_settings);
+ } else if (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) {
+ return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed);
+ } else if (accessPoint.isMetered()) {
+ return IconCompat.createWithResource(mContext, R.drawable.ic_friction_money);
+ }
+ return null;
+ }
+
+ private PendingIntent getAccessPointAction(AccessPoint accessPoint) {
+ final Bundle extras = new Bundle();
+ accessPoint.saveWifiState(extras);
+
+ Intent intent;
+ if (accessPoint.isActive()) {
+ intent = new SubSettingLauncher(mContext)
+ .setTitleRes(R.string.pref_title_network_details)
+ .setDestination(WifiNetworkDetailsFragment.class.getName())
+ .setArguments(extras)
+ .setSourceMetricsCategory(MetricsEvent.WIFI)
+ .toIntent();
+ } else {
+ intent = new Intent(mContext, WifiDialogActivity.class);
+ intent.putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras);
+ }
+ return PendingIntent.getActivity(mContext, accessPoint.hashCode() /* requestCode */,
+ intent, 0 /* flags */);
}
/**
return PendingIntent.getActivity(mContext, 0 /* requestCode */,
intent, 0 /* flags */);
}
+
+ @Override
+ public SliceBackgroundWorker getBackgroundWorker() {
+ return WifiScanWorker.getInstance(mContext, WIFI_URI);
+ }
+
+ private static class WifiScanWorker extends SliceBackgroundWorker<AccessPoint>
+ implements WifiTracker.WifiListener {
+
+ private static WifiScanWorker mWifiScanWorker;
+
+ private final Context mContext;
+
+ private WifiTracker mWifiTracker;
+ private WifiManager mWifiManager;
+
+ private WifiScanWorker(Context context, Uri uri) {
+ super(context.getContentResolver(), uri);
+ mContext = context;
+ }
+
+ public static WifiScanWorker getInstance(Context context, Uri uri) {
+ if (mWifiScanWorker == null) {
+ mWifiScanWorker = new WifiScanWorker(context, uri);
+ }
+ return mWifiScanWorker;
+ }
+
+ @Override
+ protected void onSlicePinned() {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ mWifiTracker = new WifiTracker(mContext, this, true, true);
+ mWifiManager = mWifiTracker.getManager();
+ mWifiTracker.onStart();
+ onAccessPointsChanged();
+ });
+ }
+
+ @Override
+ protected void onSliceUnpinned() {
+ mWifiTracker.onStop();
+ mWifiTracker.onDestroy();
+ mWifiScanWorker = null;
+ }
+
+ @Override
+ public void onWifiStateChanged(int state) {
+ }
+
+ @Override
+ public void onConnectedChanged() {
+ }
+
+ @Override
+ public void onAccessPointsChanged() {
+ // in case state has changed
+ if (!mWifiManager.isWifiEnabled()) {
+ updateResults(null);
+ return;
+ }
+ // AccessPoints are sorted by the WifiTracker
+ final List<AccessPoint> accessPoints = mWifiTracker.getAccessPoints();
+ final List<AccessPoint> resultList = new ArrayList<>();
+ for (AccessPoint ap : accessPoints) {
+ if (ap.isReachable()) {
+ resultList.add(ap);
+ }
+ }
+ updateResults(resultList);
+ }
+ }
}
mProvider.mSliceWeakDataCache = new HashMap<>();
mProvider.mSliceDataCache = new HashMap<>();
mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext);
- mProvider.mCustomSliceManager = new CustomSliceManager(mContext);
+ mProvider.mCustomSliceManager = spy(new CustomSliceManager(mContext));
when(mProvider.getContext()).thenReturn(mContext);
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
mProvider.onSlicePinned(uri);
}
+ private SliceBackgroundWorker initBackgroundWorker(Uri uri) {
+ final SliceBackgroundWorker worker = spy(new SliceBackgroundWorker(
+ mContext.getContentResolver(), uri) {
+ @Override
+ public void onSlicePinned() {
+ }
+
+ @Override
+ public void onSliceUnpinned() {
+ }
+ });
+ final WifiSlice wifiSlice = spy(new WifiSlice(mContext));
+ when(wifiSlice.getBackgroundWorker()).thenReturn(worker);
+ when(mProvider.mCustomSliceManager.getSliceableFromUri(uri)).thenReturn(wifiSlice);
+ return worker;
+ }
+
+ @Test
+ public void onSlicePinned_backgroundWorker_started() {
+ final Uri uri = WifiSlice.WIFI_URI;
+ final SliceBackgroundWorker worker = initBackgroundWorker(uri);
+
+ mProvider.onSlicePinned(uri);
+
+ verify(worker).onSlicePinned();
+ }
+
+ @Test
+ public void onSlicePinned_backgroundWorker_stopped() {
+ final Uri uri = WifiSlice.WIFI_URI;
+ final SliceBackgroundWorker worker = initBackgroundWorker(uri);
+
+ mProvider.onSlicePinned(uri);
+ mProvider.onSliceUnpinned(uri);
+
+ verify(worker).onSliceUnpinned();
+ }
+
@Test
public void grantWhitelistedPackagePermissions_noWhitelist_shouldNotGrant() {
final List<Uri> uris = new ArrayList<>();