1 /* 2 * Copyright (C) 2015 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.Strings.PREVIEW_WORK_FILE_ACCESSIBILITY; 20 import static com.android.documentsui.DevicePolicyResources.Strings.UNDEFINED; 21 22 import android.app.admin.DevicePolicyManager; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.os.Build; 26 import android.os.Bundle; 27 import android.view.KeyEvent; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewPropertyAnimator; 33 import android.widget.ImageView; 34 35 import androidx.annotation.RequiresApi; 36 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 37 import androidx.recyclerview.widget.RecyclerView; 38 39 import com.android.documentsui.ConfigStore; 40 import com.android.documentsui.DocumentsApplication; 41 import com.android.documentsui.R; 42 import com.android.documentsui.UserManagerState; 43 import com.android.documentsui.base.Shared; 44 import com.android.documentsui.base.State; 45 import com.android.documentsui.base.UserId; 46 import com.android.modules.utils.build.SdkLevel; 47 48 import java.util.function.Function; 49 50 import javax.annotation.Nullable; 51 52 /** 53 * ViewHolder of a document item within a RecyclerView. 54 */ 55 public abstract class DocumentHolder 56 extends RecyclerView.ViewHolder implements View.OnKeyListener { 57 58 static final float DISABLED_ALPHA = 0.3f; 59 60 protected final Context mContext; 61 62 protected @Nullable String mModelId; 63 64 protected @State.ActionType int mAction; 65 protected final ConfigStore mConfigStore; 66 67 // See #addKeyEventListener for details on the need for this field. 68 private KeyboardEventListener<DocumentItemDetails> mKeyEventListener; 69 70 private final DocumentItemDetails mDetails; 71 DocumentHolder(Context context, ViewGroup parent, int layout, ConfigStore configStore)72 public DocumentHolder(Context context, ViewGroup parent, int layout, ConfigStore configStore) { 73 this(context, inflateLayout(context, parent, layout), configStore); 74 } 75 DocumentHolder(Context context, View item, ConfigStore configStore)76 public DocumentHolder(Context context, View item, ConfigStore configStore) { 77 super(item); 78 79 itemView.setOnKeyListener(this); 80 81 mContext = context; 82 mDetails = new DocumentItemDetails(this); 83 mConfigStore = configStore; 84 } 85 86 /** 87 * Binds the view to the given item data. 88 */ bind(Cursor cursor, String modelId)89 public abstract void bind(Cursor cursor, String modelId); 90 getModelId()91 public String getModelId() { 92 return mModelId; 93 } 94 95 /** 96 * Makes the associated item view appear selected. Note that this merely affects the appearance 97 * of the view, it doesn't actually select the item. 98 * TODO: Use the DirectoryItemAnimator instead of manually controlling animation using a boolean 99 * flag. 100 * 101 * @param animate Whether or not to animate the change. Only selection changes initiated by the 102 * selection manager should be animated. See 103 * {@link ModelBackedDocumentsAdapter#onBindViewHolder(DocumentHolder, int, 104 * java.util.List)} 105 */ setSelected(boolean selected, boolean animate)106 public void setSelected(boolean selected, boolean animate) { 107 itemView.setActivated(selected); 108 itemView.setSelected(selected); 109 } 110 setEnabled(boolean enabled)111 public void setEnabled(boolean enabled) { 112 setEnabledRecursive(itemView, enabled); 113 } 114 setAction(@tate.ActionType int action)115 public void setAction(@State.ActionType int action) { 116 mAction = action; 117 } 118 119 /** 120 * @param show boolean denoting whether the current profile is non-personal 121 * @param clickCallback call back function 122 */ bindPreviewIcon(boolean show, Function<View, Boolean> clickCallback)123 public void bindPreviewIcon(boolean show, Function<View, Boolean> clickCallback) { 124 } 125 126 /** 127 * @param show boolean denoting whether the current profile is managed 128 */ bindBriefcaseIcon(boolean show)129 public void bindBriefcaseIcon(boolean show) { 130 } 131 132 /** 133 * Binds profile badge icon to the documents thumbnail 134 * 135 * @param show boolean denoting whether the current profile is non-personal/parent 136 * @param userIdIdentifier user id of the profile the document belongs to 137 */ bindProfileIcon(boolean show, int userIdIdentifier)138 public void bindProfileIcon(boolean show, int userIdIdentifier) { 139 } 140 141 @Override onKey(View v, int keyCode, KeyEvent event)142 public boolean onKey(View v, int keyCode, KeyEvent event) { 143 assert (mKeyEventListener != null); 144 DocumentItemDetails details = getItemDetails(); 145 return (details == null) 146 ? false 147 : mKeyEventListener.onKey(details, keyCode, event); 148 } 149 150 /** 151 * Installs a delegate to receive keyboard input events. This arrangement is necessitated 152 * by the fact that a single listener cannot listen to all keyboard events 153 * on RecyclerView (our parent view). Not sure why this is, but have been 154 * assured it is the case. 155 * 156 * <p>Ideally we'd not involve DocumentHolder in propagation of events like this. 157 */ addKeyEventListener(KeyboardEventListener<DocumentItemDetails> listener)158 public void addKeyEventListener(KeyboardEventListener<DocumentItemDetails> listener) { 159 assert (mKeyEventListener == null); 160 mKeyEventListener = listener; 161 } 162 inDragRegion(MotionEvent event)163 public boolean inDragRegion(MotionEvent event) { 164 return false; 165 } 166 inSelectRegion(MotionEvent event)167 public boolean inSelectRegion(MotionEvent event) { 168 return false; 169 } 170 inPreviewIconRegion(MotionEvent event)171 public boolean inPreviewIconRegion(MotionEvent event) { 172 return false; 173 } 174 getItemDetails()175 public DocumentItemDetails getItemDetails() { 176 return mDetails; 177 } 178 setEnabledRecursive(View itemView, boolean enabled)179 static void setEnabledRecursive(View itemView, boolean enabled) { 180 if (itemView == null || itemView.isEnabled() == enabled) { 181 return; 182 } 183 itemView.setEnabled(enabled); 184 185 if (itemView instanceof ViewGroup) { 186 final ViewGroup vg = (ViewGroup) itemView; 187 for (int i = vg.getChildCount() - 1; i >= 0; i--) { 188 setEnabledRecursive(vg.getChildAt(i), enabled); 189 } 190 } 191 } 192 193 @SuppressWarnings("TypeParameterUnusedInFormals") inflateLayout(Context context, ViewGroup parent, int layout)194 private static <V extends View> V inflateLayout(Context context, ViewGroup parent, int layout) { 195 final LayoutInflater inflater = LayoutInflater.from(context); 196 return (V) inflater.inflate(layout, parent, false); 197 } 198 fade(ImageView view, float alpha)199 static ViewPropertyAnimator fade(ImageView view, float alpha) { 200 return view.animate().setDuration(Shared.CHECK_ANIMATION_DURATION).alpha(alpha); 201 } 202 getPreviewIconContentDescription(boolean isNonPersonalProfile, String fileName, UserId userId)203 protected String getPreviewIconContentDescription(boolean isNonPersonalProfile, 204 String fileName, UserId userId) { 205 if (mConfigStore.isPrivateSpaceInDocsUIEnabled() && SdkLevel.isAtLeastS()) { 206 UserManagerState userManagerState = DocumentsApplication.getUserManagerState(mContext); 207 String profileLabel = userManagerState.getUserIdToLabelMap().get(userId); 208 return isNonPersonalProfile 209 ? itemView.getResources().getString(R.string.preview_cross_profile_file, 210 profileLabel, fileName) 211 : itemView.getResources().getString(R.string.preview_file, fileName); 212 } 213 if (SdkLevel.isAtLeastT()) { 214 return getUpdatablePreviewIconContentDescription(isNonPersonalProfile, fileName); 215 } else { 216 return itemView.getResources().getString( 217 isNonPersonalProfile ? R.string.preview_work_file : R.string.preview_file, 218 fileName); 219 } 220 } 221 222 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatablePreviewIconContentDescription( boolean isWorkProfile, String fileName)223 private String getUpdatablePreviewIconContentDescription( 224 boolean isWorkProfile, String fileName) { 225 DevicePolicyManager dpm = itemView.getContext().getSystemService( 226 DevicePolicyManager.class); 227 String updatableStringId = isWorkProfile ? PREVIEW_WORK_FILE_ACCESSIBILITY : UNDEFINED; 228 int defaultStringId = 229 isWorkProfile ? R.string.preview_work_file : R.string.preview_file; 230 return dpm.getResources().getString( 231 updatableStringId, 232 () -> itemView.getResources().getString(defaultStringId, fileName), 233 /* formatArgs= */ fileName); 234 } 235 236 protected static class PreviewAccessibilityDelegate extends View.AccessibilityDelegate { 237 private Function<View, Boolean> mCallback; 238 PreviewAccessibilityDelegate(Function<View, Boolean> clickCallback)239 public PreviewAccessibilityDelegate(Function<View, Boolean> clickCallback) { 240 super(); 241 mCallback = clickCallback; 242 } 243 244 @Override performAccessibilityAction(View host, int action, Bundle args)245 public boolean performAccessibilityAction(View host, int action, Bundle args) { 246 if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { 247 return mCallback.apply(host); 248 } 249 return super.performAccessibilityAction(host, action, args); 250 } 251 } 252 } 253