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