• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bips;
18 
19 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
20 
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.Notification;
24 import android.app.NotificationChannel;
25 import android.app.NotificationManager;
26 import android.app.PendingIntent;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.pm.PackageManager;
31 import android.graphics.drawable.Icon;
32 import android.location.LocationManager;
33 import android.provider.Settings;
34 import android.util.Log;
35 import android.widget.Toast;
36 
37 import com.android.bips.ui.AddPrintersActivity;
38 import com.android.bips.ui.AddPrintersFragment;
39 
40 /**
41  * Manage Wi-Fi Direct permission requirements and state.
42  */
43 public class P2pPermissionManager {
44     private static final String TAG = P2pPermissionManager.class.getCanonicalName();
45     private static final boolean DEBUG = false;
46 
47     private static final String CHANNEL_ID_CONNECTIONS = "connections";
48     public static final int REQUEST_P2P_PERMISSION_CODE = 1000;
49     public static final int REQUEST_LOCATION_ENABLE = 1001;
50 
51     private static final String STATE_KEY = "state";
52 
53     private static final P2pPermissionRequest sFinishedRequest = () -> { };
54 
55     private final Context mContext;
56     private final SharedPreferences mPrefs;
57     private final NotificationManager mNotificationManager;
58 
P2pPermissionManager(Context context)59     public P2pPermissionManager(Context context) {
60         mContext = context;
61         mPrefs = mContext.getSharedPreferences(TAG, 0);
62         mNotificationManager = mContext.getSystemService(NotificationManager.class);
63     }
64 
65     /**
66      * Reset any temporary modes.
67      */
reset()68     public void reset() {
69         if (getState() == State.TEMPORARILY_DISABLED) {
70             setState(State.DENIED);
71         }
72     }
73 
74     /**
75      * Update the current P2P permissions request state.
76      */
setState(State state)77     public void setState(State state) {
78         if (DEBUG) Log.d(TAG, "State from " + mPrefs.getString(STATE_KEY, "?") + " to " + state);
79         mPrefs.edit().putString(STATE_KEY, state.name()).apply();
80     }
81 
82     /**
83      * Return true if P2P features are enabled.
84      */
isP2pEnabled()85     public boolean isP2pEnabled() {
86         return getState() == State.ALLOWED;
87     }
88 
89     /**
90      * The user has made a permissions-related choice.
91      */
applyPermissionChange(boolean permanent)92     public void applyPermissionChange(boolean permanent) {
93         closeNotification();
94         if (hasP2pPermission()) {
95             setState(State.ALLOWED);
96         } else if (getState() != State.DISABLED) {
97             if (permanent) {
98                 setState(State.DISABLED);
99             } else {
100                 // Inform the user and don't try again for the rest of this session.
101                 setState(State.TEMPORARILY_DISABLED);
102                 Toast.makeText(mContext, R.string.wifi_direct_permission_rationale,
103                         Toast.LENGTH_LONG).show();
104             }
105         }
106     }
107 
108     /**
109      * Return true if the user has granted P2P-related permission.
110      */
hasP2pPermission()111     private boolean hasP2pPermission() {
112         return mContext.checkSelfPermission(ACCESS_FINE_LOCATION)
113                 == PackageManager.PERMISSION_GRANTED;
114     }
115 
116     /**
117      * Request P2P permission from the user, until the user makes a selection or the returned
118      * {@link P2pPermissionRequest} is closed.
119      *
120      * Note: if requested on behalf of an Activity, the Activity MUST call
121      * {@link P2pPermissionManager#applyPermissionChange(boolean)} whenever
122      * {@link Activity#onRequestPermissionsResult(int, String[], int[])} is called with code
123      * {@link P2pPermissionManager#REQUEST_P2P_PERMISSION_CODE}.
124      */
request(boolean explain, P2pPermissionListener listener)125     public P2pPermissionRequest request(boolean explain, P2pPermissionListener listener) {
126         // Check current permission level
127         State state = getState();
128 
129         if (DEBUG) Log.d(TAG, "request() state=" + state);
130 
131         if (state.isTerminal()) {
132             listener.onP2pPermissionComplete(state == State.ALLOWED);
133             // Nothing to close because no listener registered.
134             return sFinishedRequest;
135         }
136 
137         SharedPreferences.OnSharedPreferenceChangeListener preferenceListener =
138                 listenForPreferenceChanges(listener);
139 
140         if (mContext instanceof Activity) {
141             Activity activity = (Activity) mContext;
142             if (!isLocationEnabled()) {
143                 requestLocation(activity);
144             } else if (!hasP2pPermission()) {
145                 if (explain && activity.shouldShowRequestPermissionRationale(
146                         ACCESS_FINE_LOCATION)) {
147                     explain(activity);
148                 } else {
149                     request(activity);
150                 }
151             }
152         } else {
153             showNotification();
154         }
155 
156         return () -> {
157             // Allow the caller to close this request if it no longer cares about the result
158             closeNotification();
159             mPrefs.unregisterOnSharedPreferenceChangeListener(preferenceListener);
160         };
161     }
162 
163     /**
164      * Use the activity to request permissions if possible.
165      */
request(Activity activity)166     private void request(Activity activity) {
167         activity.requestPermissions(new String[]{ACCESS_FINE_LOCATION},
168                     REQUEST_P2P_PERMISSION_CODE);
169     }
170 
explain(Activity activity)171     private void explain(Activity activity) {
172         // User denied, but asked us to use P2P, so explain and redirect to settings
173         new AlertDialog.Builder(activity, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert)
174                 .setMessage(mContext.getString(R.string.wifi_direct_permission_rationale))
175                 .setPositiveButton(R.string.fix, (dialog, which) -> request(activity))
176                 .show();
177     }
178 
179     /**
180      * Request location services be enabled globally.
181      */
requestLocation(Activity activity)182     private void requestLocation(Activity activity) {
183         new AlertDialog.Builder(activity, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert)
184             .setMessage(mContext.getString(R.string.wifi_direct_location_rationale))
185             .setPositiveButton(R.string.enable_location, (dialog, which) ->
186                 activity.startActivityForResult(new Intent(
187                         Settings.ACTION_LOCATION_SOURCE_SETTINGS), REQUEST_LOCATION_ENABLE)
188             )
189             .setOnCancelListener(dialog -> {
190                 if (getState() == State.DENIED) {
191                     setState(State.TEMPORARILY_DISABLED);
192                 }
193             })
194             .show();
195     }
196 
listenForPreferenceChanges( P2pPermissionListener listener)197     private SharedPreferences.OnSharedPreferenceChangeListener listenForPreferenceChanges(
198             P2pPermissionListener listener) {
199         SharedPreferences.OnSharedPreferenceChangeListener preferenceListener =
200                 new SharedPreferences.OnSharedPreferenceChangeListener() {
201                     @Override
202                     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
203                                                           String key) {
204                         State state = getState();
205                         if (state.isTerminal() || state == State.DENIED) {
206                             listener.onP2pPermissionComplete(state == State.ALLOWED);
207                             mPrefs.unregisterOnSharedPreferenceChangeListener(this);
208                         }
209                     }
210                 };
211         mPrefs.registerOnSharedPreferenceChangeListener(preferenceListener);
212         return preferenceListener;
213     }
214 
215     /**
216      * Deliver a notification to the user.
217      */
showNotification()218     private void showNotification() {
219         // Because we are not in an activity create a notification to do the work
220         mNotificationManager.createNotificationChannel(new NotificationChannel(
221                 CHANNEL_ID_CONNECTIONS, mContext.getString(R.string.connections),
222                 NotificationManager.IMPORTANCE_HIGH));
223 
224         Intent proceedIntent = new Intent(mContext, AddPrintersActivity.class);
225         proceedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
226         proceedIntent.putExtra(AddPrintersFragment.EXTRA_FIX_P2P_PERMISSION, true);
227         PendingIntent proceedPendingIntent = PendingIntent.getActivity(mContext, 0,
228                 proceedIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
229         Notification.Action fixAction = new Notification.Action.Builder(
230                 Icon.createWithResource(mContext, R.drawable.ic_printservice),
231                 mContext.getString(R.string.fix), proceedPendingIntent).build();
232 
233         Intent cancelIntent = new Intent(mContext, BuiltInPrintService.class)
234                 .setAction(BuiltInPrintService.ACTION_P2P_PERMISSION_CANCEL);
235         PendingIntent cancelPendingIndent = PendingIntent.getService(mContext,
236                 BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, cancelIntent,
237                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
238 
239         Intent disableIntent = new Intent(mContext, BuiltInPrintService.class)
240                 .setAction(BuiltInPrintService.ACTION_P2P_DISABLE);
241         PendingIntent disablePendingIndent = PendingIntent.getService(mContext,
242                 BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, disableIntent,
243                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
244         Notification.Action disableAction = new Notification.Action.Builder(
245                 Icon.createWithResource(mContext, R.drawable.ic_printservice),
246                 mContext.getString(R.string.disable_wifi_direct), disablePendingIndent).build();
247 
248         Notification notification = new Notification.Builder(mContext, CHANNEL_ID_CONNECTIONS)
249                 .setSmallIcon(R.drawable.ic_printservice)
250                 .setContentText(mContext.getString(R.string.wifi_direct_problem))
251                 .setStyle(new Notification.BigTextStyle().bigText(
252                         mContext.getString(R.string.wifi_direct_problem)))
253                 .setAutoCancel(true)
254                 .setContentIntent(proceedPendingIntent)
255                 .setDeleteIntent(cancelPendingIndent)
256                 .addAction(fixAction)
257                 .addAction(disableAction)
258                 .build();
259 
260         mNotificationManager.notify(BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, notification);
261     }
262 
263     /**
264      * Return the current {@link State}.
265      */
getState()266     public State getState() {
267         // Look up stored state
268         String stateString = mPrefs.getString(STATE_KEY, State.DENIED.name());
269         State state = State.valueOf(stateString);
270 
271         if (state == State.DISABLED) {
272             // If disabled do no further checking
273             return state;
274         }
275 
276         boolean allowed = isLocationEnabled() && hasP2pPermission();
277         if (allowed && state != State.ALLOWED) {
278             // Upgrade state if now allowed
279             state = State.ALLOWED;
280             setState(state);
281         } else if (!allowed && state == State.ALLOWED) {
282             state = State.DENIED;
283             setState(state);
284         }
285         return state;
286     }
287 
288     /**
289      * Return true if location services are enabled.
290      */
isLocationEnabled()291     private boolean isLocationEnabled() {
292         LocationManager manager = (LocationManager) mContext.getSystemService(
293                 Context.LOCATION_SERVICE);
294         return manager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER);
295     }
296 
297     /**
298      * Close any outstanding notification.
299      */
closeNotification()300     void closeNotification() {
301         mNotificationManager.cancel(BuiltInPrintService.P2P_PERMISSION_REQUEST_ID);
302     }
303 
304     /**
305      * The current P2P permission request state.
306      */
307     public enum State {
308         // The user has not granted permissions.
309         DENIED,
310         // The user did not grant permissions this time but try again next time.
311         TEMPORARILY_DISABLED,
312         // The user explicitly disabled or chose not to enable P2P.
313         DISABLED,
314         // Permissions are granted.
315         ALLOWED;
316 
317         /** Return true if the user {@link State} is at a final permissions state. */
isTerminal()318         public boolean isTerminal() {
319             return this != DENIED;
320         }
321     }
322 
323     /**
324      * Listener for determining when a P2P permission request is complete.
325      */
326     public interface P2pPermissionListener {
327         /**
328          * Invoked when it is known that the user has allowed or denied the permission request.
329          */
onP2pPermissionComplete(boolean allowed)330         void onP2pPermissionComplete(boolean allowed);
331     }
332 
333     /**
334      * A closeable request for grant of P2P permissions.
335      */
336     public interface P2pPermissionRequest extends AutoCloseable {
337         @Override
close()338         void close();
339     }
340 }
341