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