/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.dirlist; import static com.android.documentsui.DevicePolicyResources.Drawables.Style.OUTLINE; import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_OFF_ICON; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_PERSONAL_MESSAGE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_PERSONAL_TITLE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_WORK_MESSAGE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SAVE_TO_WORK_TITLE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_PERSONAL_FILES_MESSAGE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_PERSONAL_FILES_TITLE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_WORK_FILES_MESSAGE; import static com.android.documentsui.DevicePolicyResources.Strings.CANT_SELECT_WORK_FILES_TITLE; import static com.android.documentsui.DevicePolicyResources.Strings.CROSS_PROFILE_NOT_ALLOWED_MESSAGE; import static com.android.documentsui.DevicePolicyResources.Strings.CROSS_PROFILE_NOT_ALLOWED_TITLE; import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE_OFF_ENABLE_BUTTON; import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE_OFF_ERROR_TITLE; import android.Manifest; import android.app.AuthenticationRequiredException; import android.app.admin.DevicePolicyManager; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.documentsui.CrossProfileException; import com.android.documentsui.CrossProfileNoPermissionException; import com.android.documentsui.CrossProfileQuietModeException; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.Metrics; import com.android.documentsui.Model.Update; import com.android.documentsui.R; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.dirlist.DocumentsAdapter.Environment; import com.android.modules.utils.build.SdkLevel; /** * Data object used by {@link InflateMessageDocumentHolder} and {@link HeaderMessageDocumentHolder}. */ abstract class Message { protected final Environment mEnv; // If the message has a button, this will be the default button call back. protected final Runnable mDefaultCallback; // If a message has a new callback when updated, this field should be updated. protected @Nullable Runnable mCallback; private @Nullable CharSequence mMessageTitle; private @Nullable CharSequence mMessageString; private @Nullable CharSequence mButtonString; private @Nullable Drawable mIcon; private boolean mShouldShow = false; protected boolean mShouldKeep = false; protected int mLayout; Message(Environment env, Runnable defaultCallback) { mEnv = env; mDefaultCallback = defaultCallback; } abstract void update(Update Event); protected void update(@Nullable CharSequence messageTitle, CharSequence messageString, @Nullable CharSequence buttonString, Drawable icon) { if (messageString == null) { return; } mMessageTitle = messageTitle; mMessageString = messageString; mButtonString = buttonString; mIcon = icon; mShouldShow = true; } void reset() { mMessageString = null; mIcon = null; mShouldShow = false; mLayout = 0; } void runCallback() { if (mCallback != null) { mCallback.run(); } else { mDefaultCallback.run(); } } Drawable getIcon() { return mIcon; } int getLayout() { return mLayout; } boolean shouldShow() { return mShouldShow; } /** * Return this message should keep showing or not. * @return true if this message should keep showing. */ boolean shouldKeep() { return mShouldKeep; } CharSequence getTitleString() { return mMessageTitle; } CharSequence getMessageString() { return mMessageString; } CharSequence getButtonString() { return mButtonString; } final static class HeaderMessage extends Message { private static final String TAG = "HeaderMessage"; HeaderMessage(Environment env, Runnable callback) { super(env, callback); } @Override void update(Update event) { reset(); // Error gets first dibs ... for now // TODO: These should be different Message objects getting updated instead of // overwriting. if (event.hasAuthenticationException()) { updateToAuthenticationExceptionHeader(event); } else if (mEnv.getModel().error != null) { update(null, mEnv.getModel().error, null, mEnv.getContext().getDrawable(R.drawable.ic_dialog_alert)); } else if (mEnv.getModel().info != null) { update(null, mEnv.getModel().info, null, mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); } else if (mEnv.getDisplayState().action == State.ACTION_OPEN_TREE && mEnv.getDisplayState().stack.peek() != null && mEnv.getDisplayState().stack.peek().isBlockedFromTree() && mEnv.getDisplayState().restrictScopeStorage) { updateBlockFromTreeMessage(); mCallback = () -> { mEnv.getActionHandler().showCreateDirectoryDialog(); }; } } private void updateToAuthenticationExceptionHeader(Update event) { assert(mEnv.getFeatures().isRemoteActionsEnabled()); RootInfo root = mEnv.getDisplayState().stack.getRoot(); String appName = DocumentsApplication.getProvidersCache( mEnv.getContext()).getApplicationName(root.userId, root.authority); update(null, mEnv.getContext().getString(R.string.authentication_required, appName), mEnv.getContext().getResources().getText(R.string.sign_in), mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); mCallback = () -> { AuthenticationRequiredException exception = (AuthenticationRequiredException) event.getException(); mEnv.getActionHandler().startAuthentication(exception.getUserAction()); }; } private void updateBlockFromTreeMessage() { mShouldKeep = true; update(mEnv.getContext().getString(R.string.directory_blocked_header_title), mEnv.getContext().getString(R.string.directory_blocked_header_subtitle), mEnv.getContext().getString(R.string.create_new_folder_button), mEnv.getContext().getDrawable(R.drawable.ic_dialog_info)); } } final static class InflateMessage extends Message { private final boolean mCanModifyQuietMode; InflateMessage(Environment env, Runnable callback) { super(env, callback); mCanModifyQuietMode = mEnv.getContext().checkSelfPermission(Manifest.permission.MODIFY_QUIET_MODE) == PackageManager.PERMISSION_GRANTED; } @Override void update(Update event) { reset(); if (event.hasCrossProfileException()) { CrossProfileException e = (CrossProfileException) event.getException(); Metrics.logCrossProfileEmptyState(e); if (e instanceof CrossProfileQuietModeException) { updateToQuietModeErrorMessage( ((CrossProfileQuietModeException) event.getException()).mUserId); } else if (event.getException() instanceof CrossProfileNoPermissionException) { updateToCrossProfileNoPermissionErrorMessage(); } else { updateToInflatedErrorMessage(); } } else if (event.hasException() && !event.hasAuthenticationException()) { updateToInflatedErrorMessage(); } else if (event.hasAuthenticationException()) { updateToCantDisplayContentMessage(); } else if (mEnv.getModel().getModelIds().length == 0) { updateToInflatedEmptyMessage(); } } private void updateToQuietModeErrorMessage(UserId userId) { mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; CharSequence buttonText = null; if (mCanModifyQuietMode) { buttonText = getEnterpriseString( WORK_PROFILE_OFF_ENABLE_BUTTON, R.string.quiet_mode_button); mCallback = () -> mEnv.getActionHandler().requestQuietModeDisabled( mEnv.getDisplayState().stack.getRoot(), userId); } update( getEnterpriseString( WORK_PROFILE_OFF_ERROR_TITLE, R.string.quiet_mode_error_title), /* messageString= */ "", buttonText, getWorkProfileOffIcon()); } private void updateToCrossProfileNoPermissionErrorMessage() { mLayout = InflateMessageDocumentHolder.LAYOUT_CROSS_PROFILE_ERROR; update(getCrossProfileNoPermissionErrorTitle(), getCrossProfileNoPermissionErrorMessage(), /* buttonString= */ null, mEnv.getContext().getDrawable(R.drawable.share_off)); } private CharSequence getCrossProfileNoPermissionErrorTitle() { boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); switch (mEnv.getDisplayState().action) { case State.ACTION_GET_CONTENT: case State.ACTION_OPEN: case State.ACTION_OPEN_TREE: return currentUserIsSystem ? getEnterpriseString( CANT_SELECT_WORK_FILES_TITLE, R.string.cant_select_work_files_error_title) : getEnterpriseString( CANT_SELECT_PERSONAL_FILES_TITLE, R.string.cant_select_personal_files_error_title); case State.ACTION_CREATE: return currentUserIsSystem ? getEnterpriseString( CANT_SAVE_TO_WORK_TITLE, R.string.cant_save_to_work_error_title) : getEnterpriseString( CANT_SAVE_TO_PERSONAL_TITLE, R.string.cant_save_to_personal_error_title); } return getEnterpriseString( CROSS_PROFILE_NOT_ALLOWED_TITLE, R.string.cross_profile_action_not_allowed_title); } private CharSequence getCrossProfileNoPermissionErrorMessage() { boolean currentUserIsSystem = UserId.CURRENT_USER.isSystem(); switch (mEnv.getDisplayState().action) { case State.ACTION_GET_CONTENT: case State.ACTION_OPEN: case State.ACTION_OPEN_TREE: return currentUserIsSystem ? getEnterpriseString( CANT_SELECT_WORK_FILES_MESSAGE, R.string.cant_select_work_files_error_message) : getEnterpriseString( CANT_SELECT_PERSONAL_FILES_MESSAGE, R.string.cant_select_personal_files_error_message); case State.ACTION_CREATE: return currentUserIsSystem ? getEnterpriseString( CANT_SAVE_TO_WORK_MESSAGE, R.string.cant_save_to_work_error_message) : getEnterpriseString( CANT_SAVE_TO_PERSONAL_MESSAGE, R.string.cant_save_to_personal_error_message); } return getEnterpriseString( CROSS_PROFILE_NOT_ALLOWED_MESSAGE, R.string.cross_profile_action_not_allowed_message); } private void updateToInflatedErrorMessage() { update(null, mEnv.getContext().getResources().getText(R.string.query_error), null, mEnv.getContext().getDrawable(R.drawable.hourglass)); } private void updateToCantDisplayContentMessage() { update(null, mEnv.getContext().getResources().getText(R.string.cant_display_content), null, mEnv.getContext().getDrawable(R.drawable.empty)); } private void updateToInflatedEmptyMessage() { final CharSequence message; if (mEnv.isInSearchMode()) { message = String.format( String.valueOf( mEnv.getContext().getResources().getText(R.string.no_results)), mEnv.getDisplayState().stack.getRoot().title); } else { message = mEnv.getContext().getResources().getText(R.string.empty); } update(null, message, null, mEnv.getContext().getDrawable(R.drawable.empty)); } private String getEnterpriseString(String updatableStringId, int defaultStringId) { if (SdkLevel.isAtLeastT()) { return getUpdatableEnterpriseString(updatableStringId, defaultStringId); } else { return mEnv.getContext().getString(defaultStringId); } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { DevicePolicyManager dpm = mEnv.getContext().getSystemService( DevicePolicyManager.class); return dpm.getResources().getString( updatableStringId, () -> mEnv.getContext().getString(defaultStringId)); } private Drawable getWorkProfileOffIcon() { if (SdkLevel.isAtLeastT()) { return getUpdatableWorkProfileIcon(); } else { return mEnv.getContext().getDrawable(R.drawable.work_off); } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) private Drawable getUpdatableWorkProfileIcon() { DevicePolicyManager dpm = mEnv.getContext().getSystemService( DevicePolicyManager.class); return dpm.getResources().getDrawable( WORK_PROFILE_OFF_ICON, OUTLINE, () -> mEnv.getContext().getDrawable(R.drawable.work_off)); } } }