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.AuthenticationRequiredException; 36 import android.app.admin.DevicePolicyManager; 37 import android.content.pm.PackageManager; 38 import android.graphics.drawable.Drawable; 39 import android.os.Build; 40 41 import androidx.annotation.Nullable; 42 import androidx.annotation.RequiresApi; 43 44 import com.android.documentsui.CrossProfileException; 45 import com.android.documentsui.CrossProfileNoPermissionException; 46 import com.android.documentsui.CrossProfileQuietModeException; 47 import com.android.documentsui.DocumentsApplication; 48 import com.android.documentsui.Metrics; 49 import com.android.documentsui.Model.Update; 50 import com.android.documentsui.R; 51 import com.android.documentsui.base.RootInfo; 52 import com.android.documentsui.base.State; 53 import com.android.documentsui.base.UserId; 54 import com.android.documentsui.dirlist.DocumentsAdapter.Environment; 55 import com.android.modules.utils.build.SdkLevel; 56 57 /** 58 * Data object used by {@link InflateMessageDocumentHolder} and {@link HeaderMessageDocumentHolder}. 59 */ 60 61 abstract class Message { 62 protected final Environment mEnv; 63 // If the message has a button, this will be the default button call back. 64 protected final Runnable mDefaultCallback; 65 // If a message has a new callback when updated, this field should be updated. 66 protected @Nullable Runnable mCallback; 67 68 private @Nullable CharSequence mMessageTitle; 69 private @Nullable CharSequence mMessageString; 70 private @Nullable CharSequence mButtonString; 71 private @Nullable Drawable mIcon; 72 private boolean mShouldShow = false; 73 protected boolean mShouldKeep = false; 74 protected int mLayout; 75 Message(Environment env, Runnable defaultCallback)76 Message(Environment env, Runnable defaultCallback) { 77 mEnv = env; 78 mDefaultCallback = defaultCallback; 79 } 80 update(Update Event)81 abstract void update(Update Event); 82 update(@ullable CharSequence messageTitle, CharSequence messageString, @Nullable CharSequence buttonString, Drawable icon)83 protected void update(@Nullable CharSequence messageTitle, CharSequence messageString, 84 @Nullable CharSequence buttonString, Drawable icon) { 85 if (messageString == null) { 86 return; 87 } 88 mMessageTitle = messageTitle; 89 mMessageString = messageString; 90 mButtonString = buttonString; 91 mIcon = icon; 92 mShouldShow = true; 93 } 94 reset()95 void reset() { 96 mMessageString = null; 97 mIcon = null; 98 mShouldShow = false; 99 mLayout = 0; 100 } 101 runCallback()102 void runCallback() { 103 if (mCallback != null) { 104 mCallback.run(); 105 } else { 106 mDefaultCallback.run(); 107 } 108 } 109 getIcon()110 Drawable getIcon() { 111 return mIcon; 112 } 113 getLayout()114 int getLayout() { 115 return mLayout; 116 } 117 shouldShow()118 boolean shouldShow() { 119 return mShouldShow; 120 } 121 122 /** 123 * Return this message should keep showing or not. 124 * @return true if this message should keep showing. 125 */ shouldKeep()126 boolean shouldKeep() { 127 return mShouldKeep; 128 } 129 getTitleString()130 CharSequence getTitleString() { 131 return mMessageTitle; 132 } 133 getMessageString()134 CharSequence getMessageString() { 135 return mMessageString; 136 } 137 getButtonString()138 CharSequence getButtonString() { 139 return mButtonString; 140 } 141 142 final static class HeaderMessage extends Message { 143 144 private static final String TAG = "HeaderMessage"; 145 HeaderMessage(Environment env, Runnable callback)146 HeaderMessage(Environment env, Runnable callback) { 147 super(env, callback); 148 } 149 150 @Override update(Update event)151 void update(Update event) { 152 reset(); 153 // Error gets first dibs ... for now 154 // TODO: These should be different Message objects getting updated instead of 155 // overwriting. 156 if (event.hasAuthenticationException()) { 157 updateToAuthenticationExceptionHeader(event); 158 } else if (mEnv.getModel().error != null) { 159 update(null, mEnv.getModel().error, null, 160 mEnv.getContext().getDrawable(R.drawable.ic_dialog_alert)); 161 } else if (mEnv.getModel().info != null) { 162 update(null, mEnv.getModel().info, null, 163 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 164 } else if (mEnv.getDisplayState().action == State.ACTION_OPEN_TREE 165 && mEnv.getDisplayState().stack.peek() != null 166 && mEnv.getDisplayState().stack.peek().isBlockedFromTree() 167 && mEnv.getDisplayState().restrictScopeStorage) { 168 updateBlockFromTreeMessage(); 169 mCallback = () -> { 170 mEnv.getActionHandler().showCreateDirectoryDialog(); 171 }; 172 } 173 } 174 updateToAuthenticationExceptionHeader(Update event)175 private void updateToAuthenticationExceptionHeader(Update event) { 176 assert(mEnv.getFeatures().isRemoteActionsEnabled()); 177 178 RootInfo root = mEnv.getDisplayState().stack.getRoot(); 179 String appName = DocumentsApplication.getProvidersCache( 180 mEnv.getContext()).getApplicationName(root.userId, root.authority); 181 update(null, mEnv.getContext().getString(R.string.authentication_required, appName), 182 mEnv.getContext().getResources().getText(R.string.sign_in), 183 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 184 mCallback = () -> { 185 AuthenticationRequiredException exception = 186 (AuthenticationRequiredException) event.getException(); 187 mEnv.getActionHandler().startAuthentication(exception.getUserAction()); 188 }; 189 } 190 updateBlockFromTreeMessage()191 private void updateBlockFromTreeMessage() { 192 mShouldKeep = true; 193 update(mEnv.getContext().getString(R.string.directory_blocked_header_title), 194 mEnv.getContext().getString(R.string.directory_blocked_header_subtitle), 195 mEnv.getContext().getString(R.string.create_new_folder_button), 196 mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); 197 } 198 } 199 200 final static class InflateMessage extends Message { 201 202 private final boolean mCanModifyQuietMode; 203 InflateMessage(Environment env, Runnable callback)204 InflateMessage(Environment env, Runnable callback) { 205 super(env, callback); 206 mCanModifyQuietMode = 207 mEnv.getContext().checkSelfPermission(Manifest.permission.MODIFY_QUIET_MODE) 208 == PackageManager.PERMISSION_GRANTED; 209 } 210 211 @Override update(Update event)212 void update(Update event) { 213 reset(); 214 if (event.hasCrossProfileException()) { 215 CrossProfileException e = (CrossProfileException) event.getException(); 216 Metrics.logCrossProfileEmptyState(e); 217 if (e instanceof CrossProfileQuietModeException) { 218 updateToQuietModeErrorMessage( 219 ((CrossProfileQuietModeException) event.getException()).mUserId); 220 } else if (event.getException() instanceof CrossProfileNoPermissionException) { 221 updateToCrossProfileNoPermissionErrorMessage(); 222 } else { 223 updateToInflatedErrorMessage(); 224 } 225 } else if (event.hasException() && !event.hasAuthenticationException()) { 226 updateToInflatedErrorMessage(); 227 } else if (event.hasAuthenticationException()) { 228 updateToCantDisplayContentMessage(); 229 } else if (mEnv.getModel().getModelIds().length == 0) { 230 updateToInflatedEmptyMessage(); 231 } 232 } 233 updateToQuietModeErrorMessage(UserId userId)234 private void updateToQuietModeErrorMessage(UserId userId) { 235 mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; 236 CharSequence buttonText = null; 237 if (mCanModifyQuietMode) { 238 buttonText = getEnterpriseString( 239 WORK_PROFILE_OFF_ENABLE_BUTTON, R.string.quiet_mode_button); 240 mCallback = () -> mEnv.getActionHandler().requestQuietModeDisabled( 241 mEnv.getDisplayState().stack.getRoot(), userId); 242 } 243 update( 244 getEnterpriseString( 245 WORK_PROFILE_OFF_ERROR_TITLE, R.string.quiet_mode_error_title), 246 /* messageString= */ "", 247 buttonText, 248 getWorkProfileOffIcon()); 249 } 250 updateToCrossProfileNoPermissionErrorMessage()251 private void updateToCrossProfileNoPermissionErrorMessage() { 252 mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; 253 update(getCrossProfileNoPermissionErrorTitle(), 254 getCrossProfileNoPermissionErrorMessage(), 255 /* buttonString= */ null, 256 mEnv.getContext().getDrawable(R.drawable.share_off)); 257 } 258 getCrossProfileNoPermissionErrorTitle()259 private CharSequence getCrossProfileNoPermissionErrorTitle() { 260 boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); 261 switch (mEnv.getDisplayState().action) { 262 case State.ACTION_GET_CONTENT: 263 case State.ACTION_OPEN: 264 case State.ACTION_OPEN_TREE: 265 return currentUserIsSystem 266 ? getEnterpriseString( 267 CANT_SELECT_WORK_FILES_TITLE, 268 R.string.cant_select_work_files_error_title) 269 : getEnterpriseString( 270 CANT_SELECT_PERSONAL_FILES_TITLE, 271 R.string.cant_select_personal_files_error_title); 272 case State.ACTION_CREATE: 273 return currentUserIsSystem 274 ? getEnterpriseString( 275 CANT_SAVE_TO_WORK_TITLE, R.string.cant_save_to_work_error_title) 276 : getEnterpriseString( 277 CANT_SAVE_TO_PERSONAL_TITLE, 278 R.string.cant_save_to_personal_error_title); 279 } 280 return getEnterpriseString( 281 CROSS_PROFILE_NOT_ALLOWED_TITLE, 282 R.string.cross_profile_action_not_allowed_title); 283 } 284 getCrossProfileNoPermissionErrorMessage()285 private CharSequence getCrossProfileNoPermissionErrorMessage() { 286 boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); 287 switch (mEnv.getDisplayState().action) { 288 case State.ACTION_GET_CONTENT: 289 case State.ACTION_OPEN: 290 case State.ACTION_OPEN_TREE: 291 return currentUserIsSystem 292 ? getEnterpriseString( 293 CANT_SELECT_WORK_FILES_MESSAGE, 294 R.string.cant_select_work_files_error_message) 295 : getEnterpriseString( 296 CANT_SELECT_PERSONAL_FILES_MESSAGE, 297 R.string.cant_select_personal_files_error_message); 298 case State.ACTION_CREATE: 299 return currentUserIsSystem 300 ? getEnterpriseString( 301 CANT_SAVE_TO_WORK_MESSAGE, 302 R.string.cant_save_to_work_error_message) 303 : getEnterpriseString( 304 CANT_SAVE_TO_PERSONAL_MESSAGE, 305 R.string.cant_save_to_personal_error_message); 306 } 307 return getEnterpriseString( 308 CROSS_PROFILE_NOT_ALLOWED_MESSAGE, 309 R.string.cross_profile_action_not_allowed_message); 310 } 311 updateToInflatedErrorMessage()312 private void updateToInflatedErrorMessage() { 313 update(null, mEnv.getContext().getResources().getText(R.string.query_error), null, 314 mEnv.getContext().getDrawable(R.drawable.hourglass)); 315 } 316 updateToCantDisplayContentMessage()317 private void updateToCantDisplayContentMessage() { 318 update(null, mEnv.getContext().getResources().getText(R.string.cant_display_content), 319 null, mEnv.getContext().getDrawable(R.drawable.empty)); 320 } 321 updateToInflatedEmptyMessage()322 private void updateToInflatedEmptyMessage() { 323 final CharSequence message; 324 if (mEnv.isInSearchMode()) { 325 message = String.format( 326 String.valueOf( 327 mEnv.getContext().getResources().getText(R.string.no_results)), 328 mEnv.getDisplayState().stack.getRoot().title); 329 } else { 330 message = mEnv.getContext().getResources().getText(R.string.empty); 331 } 332 update(null, message, null, mEnv.getContext().getDrawable(R.drawable.empty)); 333 } 334 getEnterpriseString(String updatableStringId, int defaultStringId)335 private String getEnterpriseString(String updatableStringId, int defaultStringId) { 336 if (SdkLevel.isAtLeastT()) { 337 return getUpdatableEnterpriseString(updatableStringId, defaultStringId); 338 } else { 339 return mEnv.getContext().getString(defaultStringId); 340 } 341 } 342 343 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableEnterpriseString(String updatableStringId, int defaultStringId)344 private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { 345 DevicePolicyManager dpm = mEnv.getContext().getSystemService( 346 DevicePolicyManager.class); 347 return dpm.getResources().getString( 348 updatableStringId, () -> mEnv.getContext().getString(defaultStringId)); 349 } 350 getWorkProfileOffIcon()351 private Drawable getWorkProfileOffIcon() { 352 if (SdkLevel.isAtLeastT()) { 353 return getUpdatableWorkProfileIcon(); 354 } else { 355 return mEnv.getContext().getDrawable(R.drawable.work_off); 356 } 357 } 358 359 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableWorkProfileIcon()360 private Drawable getUpdatableWorkProfileIcon() { 361 DevicePolicyManager dpm = mEnv.getContext().getSystemService( 362 DevicePolicyManager.class); 363 return dpm.getResources().getDrawable( 364 WORK_PROFILE_OFF_ICON, OUTLINE, 365 () -> mEnv.getContext().getDrawable(R.drawable.work_off)); 366 } 367 } 368 } 369