1 /* 2 * Copyright (C) 2016 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.documentsui.dirlist; 18 19 import static com.android.documentsui.DevicePolicyResources.Drawables.Style.OUTLINE; 20 import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_OFF_ICON; 21 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_PERSONAL_MESSAGE; 22 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_PERSONAL_TITLE; 23 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_WORK_MESSAGE; 24 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_WORK_TITLE; 25 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_PERSONAL_FILES_MESSAGE; 26 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_PERSONAL_FILES_TITLE; 27 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_WORK_FILES_MESSAGE; 28 import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_WORK_FILES_TITLE; 29 import static com.android.documentsui.DevicePolicyResources.Strings.CROSS_PROFILE_NOT_ALLOWED_MESSAGE; 30 import static com.android.documentsui.DevicePolicyResources.Strings.CROSS_PROFILE_NOT_ALLOWED_TITLE; 31 import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE_OFF_ENABLE_BUTTON; 32 import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE_OFF_ERROR_TITLE; 33 34 import android.Manifest; 35 import android.app.ActivityManager; 36 import android.app.AuthenticationRequiredException; 37 import android.app.admin.DevicePolicyManager; 38 import android.content.pm.PackageManager; 39 import android.content.pm.UserProperties; 40 import android.content.res.Resources; 41 import android.graphics.drawable.Drawable; 42 import android.os.Build; 43 import android.os.UserHandle; 44 import android.os.UserManager; 45 import android.util.Log; 46 47 import androidx.annotation.Nullable; 48 import androidx.annotation.RequiresApi; 49 50 import com.android.documentsui.ConfigStore; 51 import com.android.documentsui.CrossProfileException; 52 import com.android.documentsui.CrossProfileNoPermissionException; 53 import com.android.documentsui.CrossProfileQuietModeException; 54 import com.android.documentsui.DocumentsApplication; 55 import com.android.documentsui.Metrics; 56 import com.android.documentsui.Model.Update; 57 import com.android.documentsui.R; 58 import com.android.documentsui.base.RootInfo; 59 import com.android.documentsui.base.State; 60 import com.android.documentsui.base.UserId; 61 import com.android.documentsui.dirlist.DocumentsAdapter.Environment; 62 import com.android.modules.utils.build.SdkLevel; 63 64 import java.util.HashMap; 65 import java.util.Locale; 66 import java.util.Map; 67 68 /** 69 * Data object used by {@link InflateMessageDocumentHolder} and {@link HeaderMessageDocumentHolder}. 70 */ 71 72 abstract class Message { 73 private static final int ACCESS_CROSS_PROFILE_FILES = -1; 74 75 protected final Environment mEnv; 76 // If the message has a button, this will be the default button call back. 77 protected final Runnable mDefaultCallback; 78 // If a message has a new callback when updated, this field should be updated. 79 protected @Nullable Runnable mCallback; 80 81 private @Nullable CharSequence mMessageTitle; 82 private @Nullable CharSequence mMessageString; 83 private @Nullable CharSequence mButtonString; 84 private @Nullable Drawable mIcon; 85 private boolean mShouldShow = false; 86 protected boolean mShouldKeep = false; 87 protected int mLayout; 88 protected ConfigStore mConfigStore; 89 Message(Environment env, Runnable defaultCallback, ConfigStore configStore)90 Message(Environment env, Runnable defaultCallback, ConfigStore configStore) { 91 mEnv = env; 92 mDefaultCallback = defaultCallback; 93 mConfigStore = configStore; 94 } 95 update(Update event)96 abstract void update(Update event); 97 update(@ullable CharSequence messageTitle, CharSequence messageString, @Nullable CharSequence buttonString, Drawable icon)98 protected void update(@Nullable CharSequence messageTitle, CharSequence messageString, 99 @Nullable CharSequence buttonString, Drawable icon) { 100 if (messageString == null) { 101 return; 102 } 103 mMessageTitle = messageTitle; 104 mMessageString = messageString; 105 mButtonString = buttonString; 106 mIcon = icon; 107 mShouldShow = true; 108 } 109 reset()110 void reset() { 111 mMessageString = null; 112 mIcon = null; 113 mShouldShow = false; 114 mLayout = 0; 115 } 116 runCallback()117 void runCallback() { 118 if (mCallback != null) { 119 mCallback.run(); 120 } else { 121 mDefaultCallback.run(); 122 } 123 } 124 getIcon()125 Drawable getIcon() { 126 return mIcon; 127 } 128 getLayout()129 int getLayout() { 130 return mLayout; 131 } 132 shouldShow()133 boolean shouldShow() { 134 return mShouldShow; 135 } 136 137 /** 138 * Return this message should keep showing or not. 139 * 140 * @return true if this message should keep showing. 141 */ shouldKeep()142 boolean shouldKeep() { 143 return mShouldKeep; 144 } 145 getTitleString()146 CharSequence getTitleString() { 147 return mMessageTitle; 148 } 149 getMessageString()150 CharSequence getMessageString() { 151 return mMessageString; 152 } 153 getButtonString()154 CharSequence getButtonString() { 155 return mButtonString; 156 } 157 158 final static class HeaderMessage extends Message { 159 160 private static final String TAG = "HeaderMessage"; 161 HeaderMessage(Environment env, Runnable callback, ConfigStore configStore)162 HeaderMessage(Environment env, Runnable callback, ConfigStore configStore) { 163 super(env, callback, configStore); 164 } 165 166 @Override update(Update event)167 void update(Update event) { 168 reset(); 169 // Error gets first dibs ... for now 170 // TODO: These should be different Message objects getting updated instead of 171 // overwriting. 172 if (event.hasAuthenticationException()) { 173 updateToAuthenticationExceptionHeader(event); 174 } else if (mEnv.getModel().error != null) { 175 update(null, mEnv.getModel().error, null, 176 mEnv.getContext().getDrawable(R.drawable.ic_dialog_alert)); 177 } else if (mEnv.getModel().info != null) { 178 update(null, mEnv.getModel().info, null, 179 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 180 } else if (mEnv.getDisplayState().action == State.ACTION_OPEN_TREE 181 && mEnv.getDisplayState().stack.peek() != null 182 && mEnv.getDisplayState().stack.peek().isBlockedFromTree() 183 && mEnv.getDisplayState().restrictScopeStorage) { 184 updateBlockFromTreeMessage(); 185 mCallback = () -> { 186 mEnv.getActionHandler().showCreateDirectoryDialog(); 187 }; 188 } 189 } 190 updateToAuthenticationExceptionHeader(Update event)191 private void updateToAuthenticationExceptionHeader(Update event) { 192 assert (mEnv.getFeatures().isRemoteActionsEnabled()); 193 194 RootInfo root = mEnv.getDisplayState().stack.getRoot(); 195 String appName = DocumentsApplication.getProvidersCache( 196 mEnv.getContext()).getApplicationName(root.userId, root.authority); 197 update(null, mEnv.getContext().getString(R.string.authentication_required, appName), 198 mEnv.getContext().getResources().getText(R.string.sign_in), 199 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 200 mCallback = () -> { 201 AuthenticationRequiredException exception = 202 (AuthenticationRequiredException) event.getException(); 203 mEnv.getActionHandler().startAuthentication(exception.getUserAction()); 204 }; 205 } 206 updateBlockFromTreeMessage()207 private void updateBlockFromTreeMessage() { 208 mShouldKeep = true; 209 update(mEnv.getContext().getString(R.string.directory_blocked_header_title), 210 mEnv.getContext().getString(R.string.directory_blocked_header_subtitle), 211 mEnv.getContext().getString(R.string.create_new_folder_button), 212 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 213 } 214 } 215 216 final static class InflateMessage extends Message { 217 218 private static final String TAG = "InflateMessage"; 219 private UserId mSourceUserId = null; 220 private UserId mSelectedUserId = null; 221 private Map<UserId, String> mUserIdToLabelMap = new HashMap<>(); 222 private final boolean mCanModifyQuietMode; 223 private UserManager mUserManager = null; 224 InflateMessage(Environment env, Runnable callback, ConfigStore configStore)225 InflateMessage(Environment env, Runnable callback, ConfigStore configStore) { 226 super(env, callback, configStore); 227 mCanModifyQuietMode = 228 mEnv.getContext().checkSelfPermission(Manifest.permission.MODIFY_QUIET_MODE) 229 == PackageManager.PERMISSION_GRANTED; 230 } 231 InflateMessage(Environment env, Runnable callback, UserId sourceUserId, UserId selectedUserId, Map<UserId, String> userIdToLabelMap, UserManager userManager, ConfigStore configStore)232 InflateMessage(Environment env, Runnable callback, UserId sourceUserId, 233 UserId selectedUserId, Map<UserId, String> userIdToLabelMap, 234 UserManager userManager, ConfigStore configStore) { 235 super(env, callback, configStore); 236 mSourceUserId = sourceUserId; 237 mSelectedUserId = selectedUserId; 238 mUserIdToLabelMap = userIdToLabelMap; 239 mUserManager = userManager != null ? userManager 240 : mEnv.getContext().getSystemService(UserManager.class); 241 mCanModifyQuietMode = setCanModifyQuietMode(); 242 } 243 setCanModifyQuietMode()244 private boolean setCanModifyQuietMode() { 245 if (SdkLevel.isAtLeastV() && mConfigStore.isPrivateSpaceInDocsUIEnabled()) { 246 // Quite mode cannot be modified when DocsUi is launched from a non-foreground user 247 if (UserId.CURRENT_USER.getIdentifier() != ActivityManager.getCurrentUser()) { 248 return false; 249 } 250 251 if (mUserManager == null) { 252 Log.e(TAG, "can not obtain user manager class"); 253 return false; 254 } 255 256 UserProperties userProperties = mUserManager.getUserProperties( 257 UserHandle.of(mSelectedUserId.getIdentifier())); 258 return userProperties.getShowInQuietMode() 259 == UserProperties.SHOW_IN_QUIET_MODE_PAUSED 260 && mEnv.getContext().checkSelfPermission( 261 Manifest.permission.MODIFY_QUIET_MODE) 262 == PackageManager.PERMISSION_GRANTED; 263 } else { 264 return mEnv.getContext().checkSelfPermission(Manifest.permission.MODIFY_QUIET_MODE) 265 == PackageManager.PERMISSION_GRANTED; 266 } 267 } 268 269 @Override update(Update event)270 void update(Update event) { 271 reset(); 272 if (event.hasCrossProfileException()) { 273 CrossProfileException e = (CrossProfileException) event.getException(); 274 Metrics.logCrossProfileEmptyState(e); 275 if (e instanceof CrossProfileQuietModeException) { 276 updateToQuietModeErrorMessage( 277 ((CrossProfileQuietModeException) event.getException()).mUserId); 278 } else if (event.getException() instanceof CrossProfileNoPermissionException) { 279 updateToCrossProfileNoPermissionErrorMessage(); 280 } else { 281 updateToInflatedErrorMessage(); 282 } 283 } else if (event.hasException() && !event.hasAuthenticationException()) { 284 updateToInflatedErrorMessage(); 285 } else if (event.hasAuthenticationException()) { 286 updateToCantDisplayContentMessage(); 287 } else if (mEnv.getModel().getModelIds().length == 0) { 288 updateToInflatedEmptyMessage(); 289 } 290 } 291 updateToQuietModeErrorMessage(UserId userId)292 private void updateToQuietModeErrorMessage(UserId userId) { 293 mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; 294 String buttonText = null; 295 Resources res = null; 296 String selectedProfile = null; 297 if (mConfigStore.isPrivateSpaceInDocsUIEnabled()) { 298 res = mEnv.getContext().getResources(); 299 assert mUserIdToLabelMap != null; 300 selectedProfile = mUserIdToLabelMap.get(userId); 301 } 302 if (mCanModifyQuietMode) { 303 buttonText = mConfigStore.isPrivateSpaceInDocsUIEnabled() 304 ? res.getString(R.string.profile_quiet_mode_button, 305 selectedProfile.toLowerCase(Locale.getDefault())) 306 : getEnterpriseString( 307 WORK_PROFILE_OFF_ENABLE_BUTTON, R.string.quiet_mode_button); 308 mCallback = () -> mEnv.getActionHandler().requestQuietModeDisabled( 309 mEnv.getDisplayState().stack.getRoot(), userId); 310 } 311 312 update(mConfigStore.isPrivateSpaceInDocsUIEnabled() 313 ? res.getString(R.string.profile_quiet_mode_error_title, 314 selectedProfile) 315 : getEnterpriseString( 316 WORK_PROFILE_OFF_ERROR_TITLE, R.string.quiet_mode_error_title), 317 /* messageString= */ "", 318 buttonText, 319 getWorkProfileOffIcon()); 320 } 321 updateToCrossProfileNoPermissionErrorMessage()322 private void updateToCrossProfileNoPermissionErrorMessage() { 323 mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; 324 update(getCrossProfileNoPermissionErrorTitle(), 325 getCrossProfileNoPermissionErrorMessage(), 326 /* buttonString= */ null, 327 mEnv.getContext().getDrawable(R.drawable.share_off)); 328 } 329 getCrossProfileNoPermissionErrorTitle()330 private CharSequence getCrossProfileNoPermissionErrorTitle() { 331 switch (mEnv.getDisplayState().action) { 332 case State.ACTION_GET_CONTENT: 333 case State.ACTION_OPEN: 334 case State.ACTION_OPEN_TREE: 335 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 336 ? getErrorTitlePrivateSpaceEnabled(ACCESS_CROSS_PROFILE_FILES) 337 : getErrorTitlePrivateSpaceDisabled(ACCESS_CROSS_PROFILE_FILES); 338 case State.ACTION_CREATE: 339 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 340 ? getErrorTitlePrivateSpaceEnabled(State.ACTION_CREATE) 341 : getErrorTitlePrivateSpaceDisabled(State.ACTION_CREATE); 342 } 343 return getEnterpriseString( 344 CROSS_PROFILE_NOT_ALLOWED_TITLE, 345 R.string.cross_profile_action_not_allowed_title); 346 } 347 getErrorTitlePrivateSpaceEnabled(int action)348 private CharSequence getErrorTitlePrivateSpaceEnabled(int action) { 349 Resources res = mEnv.getContext().getResources(); 350 String selectedProfileLabel = mUserIdToLabelMap.get(mSelectedUserId); 351 if (selectedProfileLabel == null) return ""; 352 if (action == ACCESS_CROSS_PROFILE_FILES) { 353 return res.getString(R.string.cant_select_cross_profile_files_error_title, 354 selectedProfileLabel.toLowerCase(Locale.getDefault())); 355 } else if (action == State.ACTION_CREATE) { 356 return res.getString(R.string.cant_save_to_cross_profile_error_title, 357 selectedProfileLabel.toLowerCase(Locale.getDefault())); 358 } else { 359 Log.e(TAG, "Unexpected intent action received."); 360 return ""; 361 } 362 } 363 getErrorTitlePrivateSpaceDisabled(int action)364 private CharSequence getErrorTitlePrivateSpaceDisabled(int action) { 365 boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); 366 if (action == ACCESS_CROSS_PROFILE_FILES) { 367 return currentUserIsSystem 368 ? getEnterpriseString(CANT_SELECT_WORK_FILES_TITLE, 369 R.string.cant_select_work_files_error_title) 370 : getEnterpriseString(CANT_SELECT_PERSONAL_FILES_TITLE, 371 R.string.cant_select_personal_files_error_title); 372 } else if (action == State.ACTION_CREATE) { 373 return currentUserIsSystem 374 ? getEnterpriseString(CANT_SAVE_TO_WORK_TITLE, 375 R.string.cant_save_to_work_error_title) 376 : getEnterpriseString(CANT_SAVE_TO_PERSONAL_TITLE, 377 R.string.cant_save_to_personal_error_title); 378 } else { 379 Log.e(TAG, "Unexpected intent action received."); 380 return ""; 381 } 382 } 383 getCrossProfileNoPermissionErrorMessage()384 private CharSequence getCrossProfileNoPermissionErrorMessage() { 385 switch (mEnv.getDisplayState().action) { 386 case State.ACTION_GET_CONTENT: 387 case State.ACTION_OPEN: 388 case State.ACTION_OPEN_TREE: 389 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 390 ? getErrorMessagePrivateSpaceEnabled(ACCESS_CROSS_PROFILE_FILES) 391 : getErrorMessagePrivateSpaceDisabled(ACCESS_CROSS_PROFILE_FILES); 392 case State.ACTION_CREATE: 393 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 394 ? getErrorMessagePrivateSpaceEnabled(State.ACTION_CREATE) 395 : getErrorMessagePrivateSpaceDisabled(State.ACTION_CREATE); 396 397 } 398 return getEnterpriseString( 399 CROSS_PROFILE_NOT_ALLOWED_MESSAGE, 400 R.string.cross_profile_action_not_allowed_message); 401 } 402 getErrorMessagePrivateSpaceEnabled(int action)403 private CharSequence getErrorMessagePrivateSpaceEnabled(int action) { 404 Resources res = mEnv.getContext().getResources(); 405 String sourceProfileLabel = mUserIdToLabelMap.get(mSourceUserId); 406 String selectedProfileLabel = mUserIdToLabelMap.get(mSelectedUserId); 407 if (sourceProfileLabel == null || selectedProfileLabel == null) return ""; 408 if (action == ACCESS_CROSS_PROFILE_FILES) { 409 return res.getString(R.string.cant_select_cross_profile_files_error_message, 410 selectedProfileLabel.toLowerCase(Locale.getDefault()), 411 sourceProfileLabel.toLowerCase(Locale.getDefault())); 412 } else if (action == State.ACTION_CREATE) { 413 return res.getString(R.string.cant_save_to_cross_profile_error_message, 414 sourceProfileLabel.toLowerCase(Locale.getDefault()), 415 selectedProfileLabel.toLowerCase(Locale.getDefault())); 416 } else { 417 Log.e(TAG, "Unexpected intent action received."); 418 return ""; 419 } 420 } 421 getErrorMessagePrivateSpaceDisabled(int action)422 private CharSequence getErrorMessagePrivateSpaceDisabled(int action) { 423 boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); 424 if (action == ACCESS_CROSS_PROFILE_FILES) { 425 return currentUserIsSystem 426 ? getEnterpriseString(CANT_SELECT_WORK_FILES_MESSAGE, 427 R.string.cant_select_work_files_error_message) 428 : getEnterpriseString(CANT_SELECT_PERSONAL_FILES_MESSAGE, 429 R.string.cant_select_personal_files_error_message); 430 } else if (action == State.ACTION_CREATE) { 431 return currentUserIsSystem 432 ? getEnterpriseString(CANT_SAVE_TO_WORK_MESSAGE, 433 R.string.cant_save_to_work_error_message) 434 : getEnterpriseString(CANT_SAVE_TO_PERSONAL_MESSAGE, 435 R.string.cant_save_to_personal_error_message); 436 } else { 437 Log.e(TAG, "Unexpected intent action received."); 438 return ""; 439 } 440 } 441 updateToInflatedErrorMessage()442 private void updateToInflatedErrorMessage() { 443 update(null, mEnv.getContext().getResources().getText(R.string.query_error), null, 444 mEnv.getContext().getDrawable(R.drawable.hourglass)); 445 } 446 updateToCantDisplayContentMessage()447 private void updateToCantDisplayContentMessage() { 448 update(null, mEnv.getContext().getResources().getText(R.string.cant_display_content), 449 null, mEnv.getContext().getDrawable(R.drawable.empty)); 450 } 451 updateToInflatedEmptyMessage()452 private void updateToInflatedEmptyMessage() { 453 final CharSequence message; 454 if (mEnv.isInSearchMode()) { 455 message = String.format( 456 String.valueOf( 457 mEnv.getContext().getResources().getText(R.string.no_results)), 458 mEnv.getDisplayState().stack.getRoot().title); 459 } else { 460 message = mEnv.getContext().getResources().getText(R.string.empty); 461 } 462 update(null, message, null, mEnv.getContext().getDrawable(R.drawable.empty)); 463 } 464 getEnterpriseString(String updatableStringId, int defaultStringId)465 private String getEnterpriseString(String updatableStringId, int defaultStringId) { 466 if (SdkLevel.isAtLeastT()) { 467 return getUpdatableEnterpriseString(updatableStringId, defaultStringId); 468 } else { 469 return mEnv.getContext().getString(defaultStringId); 470 } 471 } 472 473 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableEnterpriseString(String updatableStringId, int defaultStringId)474 private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { 475 DevicePolicyManager dpm = mEnv.getContext().getSystemService( 476 DevicePolicyManager.class); 477 return dpm.getResources().getString( 478 updatableStringId, () -> mEnv.getContext().getString(defaultStringId)); 479 } 480 getWorkProfileOffIcon()481 private Drawable getWorkProfileOffIcon() { 482 if (SdkLevel.isAtLeastT()) { 483 return getUpdatableWorkProfileIcon(); 484 } else { 485 return mEnv.getContext().getDrawable(R.drawable.work_off); 486 } 487 } 488 489 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableWorkProfileIcon()490 private Drawable getUpdatableWorkProfileIcon() { 491 DevicePolicyManager dpm = mEnv.getContext().getSystemService( 492 DevicePolicyManager.class); 493 return dpm.getResources().getDrawable( 494 WORK_PROFILE_OFF_ICON, OUTLINE, 495 () -> mEnv.getContext().getDrawable(R.drawable.work_off)); 496 } 497 } 498 } 499