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.NEARBY_WIFI_DEVICES;
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.net.Uri;
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 PACKAGE_TAG = "package";
48     private static final String CHANNEL_ID_CONNECTIONS = "connections";
49     public static final int REQUEST_P2P_PERMISSION_CODE = 1000;
50     public static final int REQUEST_P2P_PERMISSION_ENABLE = 1001;
51 
52     private static final String STATE_KEY = "state";
53     private static final String PERMISSION_RATIONALE_KEY = "permissionRationale";
54 
55     private static final P2pPermissionRequest sFinishedRequest = () -> { };
56 
57     private final Context mContext;
58     private final SharedPreferences mPrefs;
59     private final NotificationManager mNotificationManager;
60 
P2pPermissionManager(Context context)61     public P2pPermissionManager(Context context) {
62         mContext = context;
63         mPrefs = mContext.getSharedPreferences(TAG, 0);
64         mNotificationManager = mContext.getSystemService(NotificationManager.class);
65     }
66 
67     /**
68      * Reset any temporary modes.
69      */
reset()70     public void reset() {
71         if (getState() == State.TEMPORARILY_DISABLED) {
72             setState(State.DENIED);
73         }
74     }
75 
76     /**
77      * Update the current P2P permissions request state.
78      */
setState(State state)79     public void setState(State state) {
80         if (DEBUG) Log.d(TAG, "State from " + mPrefs.getString(STATE_KEY, "?") + " to " + state);
81         mPrefs.edit().putString(STATE_KEY, state.name()).apply();
82     }
83 
84     /**
85      * Return true if P2P features are enabled.
86      */
isP2pEnabled()87     public boolean isP2pEnabled() {
88         return getState() == State.ALLOWED;
89     }
90 
91     /**
92      * The user has made a permissions-related choice.
93      */
applyPermissionChange(boolean permanent)94     public void applyPermissionChange(boolean permanent) {
95         closeNotification();
96         if (hasP2pPermission()) {
97             setState(State.ALLOWED);
98         } else if (getState() != State.DISABLED) {
99             if (permanent) {
100                 setState(State.DISABLED);
101             } else {
102                 // Inform the user and don't try again for the rest of this session.
103                 setState(State.TEMPORARILY_DISABLED);
104                 Toast.makeText(mContext, R.string.wifi_direct_permission_rationale,
105                         Toast.LENGTH_LONG).show();
106             }
107         }
108     }
109 
110     /**
111      * Return true if the user has granted P2P-related permission.
112      */
hasP2pPermission()113     private boolean hasP2pPermission() {
114         return mContext.checkSelfPermission(NEARBY_WIFI_DEVICES)
115                 == PackageManager.PERMISSION_GRANTED;
116     }
117 
118     /**
119      * Request P2P permission from the user, until the user makes a selection or the returned
120      * {@link P2pPermissionRequest} is closed.
121      *
122      * Note: if requested on behalf of an Activity, the Activity MUST call
123      * {@link P2pPermissionManager#applyPermissionChange(boolean)} whenever
124      * {@link Activity#onRequestPermissionsResult(int, String[], int[])} is called with code
125      * {@link P2pPermissionManager#REQUEST_P2P_PERMISSION_CODE}.
126      */
request(boolean explain, P2pPermissionListener listener)127     public P2pPermissionRequest request(boolean explain, P2pPermissionListener listener) {
128         // Check current permission level
129         State state = getState();
130 
131         if (DEBUG) Log.d(TAG, "request() state=" + state);
132 
133         if (state.isTerminal()) {
134             listener.onP2pPermissionComplete(state == State.ALLOWED);
135             // Nothing to close because no listener registered.
136             return sFinishedRequest;
137         }
138 
139         SharedPreferences.OnSharedPreferenceChangeListener preferenceListener =
140                 listenForPreferenceChanges(listener);
141 
142         if (mContext instanceof Activity) {
143             Activity activity = (Activity) mContext;
144             if (!hasP2pPermission()) {
145                 boolean rationale = activity.shouldShowRequestPermissionRationale(
146                         NEARBY_WIFI_DEVICES);
147                 if (explain && (rationale || mPrefs.getBoolean(PERMISSION_RATIONALE_KEY, false))) {
148                     explain(activity, rationale);
149                 } else {
150                     request(activity);
151                 }
152                 if (rationale) {
153                     // Saving the permission to prefs to handle "Don't ask again" scenario
154                     mPrefs.edit().putBoolean(PERMISSION_RATIONALE_KEY, true).apply();
155                 }
156             }
157         } else {
158             showNotification();
159         }
160 
161         return () -> {
162             // Allow the caller to close this request if it no longer cares about the result
163             closeNotification();
164             mPrefs.unregisterOnSharedPreferenceChangeListener(preferenceListener);
165         };
166     }
167 
168     /**
169      * Use the activity to request permissions if possible.
170      */
request(Activity activity)171     private void request(Activity activity) {
172         activity.requestPermissions(new String[]{NEARBY_WIFI_DEVICES},
173                 REQUEST_P2P_PERMISSION_CODE);
174     }
175 
redirectToSettings(Activity activity)176     private void redirectToSettings(Activity activity) {
177         Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
178                 Uri.fromParts(PACKAGE_TAG, activity.getPackageName(), null));
179         activity.startActivityForResult(settingsIntent, REQUEST_P2P_PERMISSION_ENABLE);
180     }
181 
explain(Activity activity, boolean rationale)182     private void explain(Activity activity, boolean rationale) {
183         // User denied, but asked us to use P2P, so explain and redirect to settings
184         new AlertDialog.Builder(activity, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert)
185                 .setMessage(mContext.getString(R.string.wifi_direct_permission_rationale))
186                 .setPositiveButton(R.string.fix, (dialog, which) -> {
187                     if (rationale) {
188                         request(activity);
189                     } else {
190                         redirectToSettings(activity);
191                     }
192                 })
193                 .show();
194     }
195 
listenForPreferenceChanges( P2pPermissionListener listener)196     private SharedPreferences.OnSharedPreferenceChangeListener listenForPreferenceChanges(
197             P2pPermissionListener listener) {
198         SharedPreferences.OnSharedPreferenceChangeListener preferenceListener =
199                 new SharedPreferences.OnSharedPreferenceChangeListener() {
200                     @Override
201                     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
202                                                           String key) {
203                         if (PERMISSION_RATIONALE_KEY.equals(key)) {
204                             return;
205                         }
206                         State state = getState();
207                         if (state.isTerminal() || state == State.DENIED) {
208                             listener.onP2pPermissionComplete(state == State.ALLOWED);
209                             mPrefs.unregisterOnSharedPreferenceChangeListener(this);
210                         }
211                     }
212                 };
213         mPrefs.registerOnSharedPreferenceChangeListener(preferenceListener);
214         return preferenceListener;
215     }
216 
217     /**
218      * Deliver a notification to the user.
219      */
showNotification()220     private void showNotification() {
221         // Because we are not in an activity create a notification to do the work
222         mNotificationManager.createNotificationChannel(new NotificationChannel(
223                 CHANNEL_ID_CONNECTIONS, mContext.getString(R.string.connections),
224                 NotificationManager.IMPORTANCE_HIGH));
225 
226         Intent proceedIntent = new Intent(mContext, AddPrintersActivity.class);
227         proceedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
228         proceedIntent.putExtra(AddPrintersFragment.EXTRA_FIX_P2P_PERMISSION, true);
229         PendingIntent proceedPendingIntent = PendingIntent.getActivity(mContext, 0,
230                 proceedIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
231         Notification.Action fixAction = new Notification.Action.Builder(
232                 Icon.createWithResource(mContext, R.drawable.ic_printservice),
233                 mContext.getString(R.string.fix), proceedPendingIntent).build();
234 
235         Intent cancelIntent = new Intent(mContext, BuiltInPrintService.class)
236                 .setAction(BuiltInPrintService.ACTION_P2P_PERMISSION_CANCEL);
237         PendingIntent cancelPendingIndent = PendingIntent.getService(mContext,
238                 BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, cancelIntent,
239                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
240 
241         Intent disableIntent = new Intent(mContext, BuiltInPrintService.class)
242                 .setAction(BuiltInPrintService.ACTION_P2P_DISABLE);
243         PendingIntent disablePendingIndent = PendingIntent.getService(mContext,
244                 BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, disableIntent,
245                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
246         Notification.Action disableAction = new Notification.Action.Builder(
247                 Icon.createWithResource(mContext, R.drawable.ic_printservice),
248                 mContext.getString(R.string.disable_wifi_direct), disablePendingIndent).build();
249 
250         Notification notification = new Notification.Builder(mContext, CHANNEL_ID_CONNECTIONS)
251                 .setSmallIcon(R.drawable.ic_printservice)
252                 .setContentText(mContext.getString(R.string.wifi_direct_problem))
253                 .setStyle(new Notification.BigTextStyle().bigText(
254                         mContext.getString(R.string.wifi_direct_problem)))
255                 .setAutoCancel(true)
256                 .setContentIntent(proceedPendingIntent)
257                 .setDeleteIntent(cancelPendingIndent)
258                 .addAction(fixAction)
259                 .addAction(disableAction)
260                 .build();
261 
262         mNotificationManager.notify(BuiltInPrintService.P2P_PERMISSION_REQUEST_ID, notification);
263     }
264 
265     /**
266      * Return the current {@link State}.
267      */
getState()268     public State getState() {
269         // Look up stored state
270         String stateString = mPrefs.getString(STATE_KEY, State.DENIED.name());
271         State state = State.valueOf(stateString);
272 
273         if (state == State.DISABLED) {
274             // If disabled do no further checking
275             return state;
276         }
277 
278         boolean allowed = hasP2pPermission();
279         if (allowed && state != State.ALLOWED) {
280             // Upgrade state if now allowed
281             state = State.ALLOWED;
282             setState(state);
283         } else if (!allowed && state == State.ALLOWED) {
284             state = State.DENIED;
285             setState(state);
286         }
287         return state;
288     }
289 
290     /**
291      * Close any outstanding notification.
292      */
closeNotification()293     void closeNotification() {
294         mNotificationManager.cancel(BuiltInPrintService.P2P_PERMISSION_REQUEST_ID);
295     }
296 
297     /**
298      * The current P2P permission request state.
299      */
300     public enum State {
301         // The user has not granted permissions.
302         DENIED,
303         // The user did not grant permissions this time but try again next time.
304         TEMPORARILY_DISABLED,
305         // The user explicitly disabled or chose not to enable P2P.
306         DISABLED,
307         // Permissions are granted.
308         ALLOWED;
309 
310         /** Return true if the user {@link State} is at a final permissions state. */
isTerminal()311         public boolean isTerminal() {
312             return this != DENIED;
313         }
314     }
315 
316     /**
317      * Listener for determining when a P2P permission request is complete.
318      */
319     public interface P2pPermissionListener {
320         /**
321          * Invoked when it is known that the user has allowed or denied the permission request.
322          */
onP2pPermissionComplete(boolean allowed)323         void onP2pPermissionComplete(boolean allowed);
324     }
325 
326     /**
327      * A closeable request for grant of P2P permissions.
328      */
329     public interface P2pPermissionRequest extends AutoCloseable {
330         @Override
close()331         void close();
332     }
333 }
334