OSDN Git Service

Slice background worker with Wi-Fi Slice
authorJason Chiu <chiujason@google.com>
Mon, 8 Oct 2018 04:06:26 +0000 (12:06 +0800)
committerJason Chiu <chiujason@google.com>
Thu, 18 Oct 2018 10:00:11 +0000 (18:00 +0800)
Test: manual

Change-Id: Ic4fdc5713f511ff80f03728c99c68fda3d0cab02

src/com/android/settings/slices/CustomSliceable.java
src/com/android/settings/slices/SettingsSliceProvider.java
src/com/android/settings/slices/SliceBackgroundWorker.java [new file with mode: 0644]
src/com/android/settings/wifi/WifiDialogActivity.java
src/com/android/settings/wifi/WifiSlice.java
tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java

index 48b5d16..e09cc73 100644 (file)
@@ -90,6 +90,16 @@ public interface CustomSliceable {
     }
 
     /**
+     * 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
index 8b975b4..395b878 100644 (file)
@@ -28,6 +28,7 @@ import android.os.StrictMode;
 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;
@@ -131,6 +132,8 @@ public class SettingsSliceProvider extends SliceProvider {
 
     final Set<Uri> mRegisteredUris = new ArraySet<>();
 
+    final Map<Uri, SliceBackgroundWorker> mWorkerMap = new ArrayMap<>();
+
     public SettingsSliceProvider() {
         super(READ_SEARCH_INDEXABLES);
     }
@@ -165,6 +168,7 @@ public class SettingsSliceProvider extends SliceProvider {
             if (filter != null) {
                 registerIntentToUri(filter, sliceUri);
             }
+            startBackgroundWorker(sliceable);
             return;
         }
 
@@ -191,6 +195,7 @@ public class SettingsSliceProvider extends SliceProvider {
             SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri);
             mRegisteredUris.remove(sliceUri);
         }
+        stopBackgroundWorker(sliceUri);
         mSliceDataCache.remove(sliceUri);
     }
 
@@ -348,6 +353,31 @@ public class SettingsSliceProvider extends SliceProvider {
         }
     }
 
+    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<>();
 
diff --git a/src/com/android/settings/slices/SliceBackgroundWorker.java b/src/com/android/settings/slices/SliceBackgroundWorker.java
new file mode 100644 (file)
index 0000000..7178ead
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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);
+        }
+    }
+}
index 63785e6..e3a03ad 100644 (file)
@@ -37,12 +37,6 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo
 
     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
@@ -51,6 +45,11 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo
      */
     @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;
 
index d0b14e3..536be6f 100644 (file)
@@ -29,6 +29,9 @@ import android.net.Uri;
 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;
 
@@ -42,8 +45,16 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 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.
@@ -96,14 +107,73 @@ public class WifiSlice implements CustomSliceable {
         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 */);
     }
 
     /**
@@ -176,4 +246,75 @@ public class WifiSlice implements CustomSliceable {
         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);
+        }
+    }
 }
index f7c6bba..19f3d32 100644 (file)
@@ -118,7 +118,7 @@ public class SettingsSliceProviderTest {
         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();
@@ -481,6 +481,44 @@ public class SettingsSliceProviderTest {
         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<>();