• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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