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