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