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