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