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.Drawables.Style.SOLID_COLORED; 20 import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; 21 import static com.android.documentsui.base.DocumentInfo.getCursorInt; 22 import static com.android.documentsui.base.DocumentInfo.getCursorLong; 23 import static com.android.documentsui.base.DocumentInfo.getCursorString; 24 import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; 25 26 import android.app.admin.DevicePolicyManager; 27 import android.content.Context; 28 import android.database.Cursor; 29 import android.graphics.drawable.Drawable; 30 import android.os.Build; 31 import android.provider.DocumentsContract.Document; 32 import android.text.format.Formatter; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.ImageView; 37 import android.widget.TextView; 38 39 import androidx.annotation.Nullable; 40 import androidx.annotation.RequiresApi; 41 42 import com.android.documentsui.ConfigStore; 43 import com.android.documentsui.DocumentsApplication; 44 import com.android.documentsui.IconUtils; 45 import com.android.documentsui.R; 46 import com.android.documentsui.base.DocumentInfo; 47 import com.android.documentsui.base.Shared; 48 import com.android.documentsui.base.State; 49 import com.android.documentsui.base.UserId; 50 import com.android.documentsui.roots.RootCursorWrapper; 51 import com.android.documentsui.ui.Views; 52 import com.android.modules.utils.build.SdkLevel; 53 54 import com.google.android.material.card.MaterialCardView; 55 56 import java.util.Map; 57 import java.util.function.Function; 58 59 final class GridDocumentHolder extends DocumentHolder { 60 61 final TextView mTitle; 62 final TextView mDate; 63 final TextView mDetails; 64 // Non-null only when useMaterial3 flag is ON. 65 final @Nullable TextView mBullet; 66 final ImageView mIconMimeLg; 67 // Null when useMaterial3 flag is ON. 68 final @Nullable ImageView mIconMimeSm; 69 final ImageView mIconThumb; 70 // Null when useMaterial3 flag is ON. 71 final @Nullable ImageView mIconCheck; 72 final ImageView mIconBadge; 73 final IconHelper mIconHelper; 74 // Null when useMaterial3 flag is ON. 75 final @Nullable View mIconLayout; 76 final View mPreviewIcon; 77 78 // This is used in as a convenience in our bind method. 79 private final DocumentInfo mDoc = new DocumentInfo(); 80 81 // Non-null only when useMaterial3 flag is ON. 82 private final @Nullable MaterialCardView mIconWrapper; 83 // It will be 0 when use_material flag is OFF. 84 private final int mThumbnailStrokeWidth; 85 GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper, ConfigStore configStore)86 GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper, 87 ConfigStore configStore) { 88 super(context, parent, R.layout.item_doc_grid, configStore); 89 90 if (isUseMaterial3FlagEnabled()) { 91 mBullet = itemView.findViewById(R.id.bullet); 92 mIconWrapper = itemView.findViewById(R.id.icon_wrapper); 93 mIconLayout = null; 94 mIconMimeSm = null; 95 mIconCheck = null; 96 mThumbnailStrokeWidth = 97 context.getResources() 98 .getDimensionPixelSize(R.dimen.thumbnail_border_width); 99 } else { 100 mBullet = null; 101 mIconWrapper = null; 102 mIconLayout = itemView.findViewById(R.id.icon); 103 mIconMimeSm = (ImageView) itemView.findViewById(R.id.icon_mime_sm); 104 mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check); 105 mThumbnailStrokeWidth = 0; 106 } 107 108 mTitle = (TextView) itemView.findViewById(android.R.id.title); 109 mDate = (TextView) itemView.findViewById(R.id.date); 110 mDetails = (TextView) itemView.findViewById(R.id.details); 111 mIconMimeLg = (ImageView) itemView.findViewById(R.id.icon_mime_lg); 112 mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb); 113 mIconBadge = (ImageView) itemView.findViewById(R.id.icon_profile_badge); 114 mPreviewIcon = itemView.findViewById(R.id.preview_icon); 115 116 if (isUseMaterial3FlagEnabled()) { 117 int clipCornerRadius = context.getResources() 118 .getDimensionPixelSize(R.dimen.thumbnail_clip_corner_radius); 119 IconUtils.applyThumbnailClipOutline( 120 mIconThumb, mThumbnailStrokeWidth, clipCornerRadius); 121 } 122 123 mIconHelper = iconHelper; 124 125 if (SdkLevel.isAtLeastT() && !mConfigStore.isPrivateSpaceInDocsUIEnabled()) { 126 setUpdatableWorkProfileIcon(context); 127 } 128 } 129 130 @RequiresApi(Build.VERSION_CODES.TIRAMISU) setUpdatableWorkProfileIcon(Context context)131 private void setUpdatableWorkProfileIcon(Context context) { 132 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 133 Drawable drawable = dpm.getResources().getDrawable(WORK_PROFILE_ICON, SOLID_COLORED, () -> 134 context.getDrawable(R.drawable.ic_briefcase)); 135 mIconBadge.setImageDrawable(drawable); 136 } 137 138 @Override setSelected(boolean selected, boolean animate)139 public void setSelected(boolean selected, boolean animate) { 140 float checkAlpha = selected ? 1f : 0f; 141 if (!isUseMaterial3FlagEnabled()) { 142 // We always want to make sure our check box disappears if we're not selected, 143 // even if the item is disabled. This is because this object can be reused 144 // and this method will be called to setup initial state. 145 if (animate) { 146 fade(mIconMimeSm, checkAlpha).start(); 147 fade(mIconCheck, checkAlpha).start(); 148 } else { 149 mIconCheck.setAlpha(checkAlpha); 150 } 151 } 152 153 154 // But it should be an error to be set to selected && be disabled. 155 if (!itemView.isEnabled()) { 156 assert (!selected); 157 } 158 159 super.setSelected(selected, animate); 160 161 if (!isUseMaterial3FlagEnabled()) { 162 if (animate) { 163 fade(mIconMimeSm, 1f - checkAlpha).start(); 164 } else { 165 mIconMimeSm.setAlpha(1f - checkAlpha); 166 } 167 } 168 169 // Do not show stroke when selected, only show stroke when not selected if it has thumbnail. 170 if (mIconWrapper != null) { 171 if (selected) { 172 mIconWrapper.setStrokeWidth(0); 173 } else if (mIconThumb.getDrawable() != null) { 174 mIconWrapper.setStrokeWidth(mThumbnailStrokeWidth); 175 } 176 } 177 } 178 179 @Override setEnabled(boolean enabled)180 public void setEnabled(boolean enabled) { 181 super.setEnabled(enabled); 182 183 float imgAlpha = enabled ? 1f : DISABLED_ALPHA; 184 185 mIconMimeLg.setAlpha(imgAlpha); 186 if (!isUseMaterial3FlagEnabled()) { 187 mIconMimeSm.setAlpha(imgAlpha); 188 } 189 mIconThumb.setAlpha(imgAlpha); 190 } 191 192 @Override bindPreviewIcon(boolean show, Function<View, Boolean> clickCallback)193 public void bindPreviewIcon(boolean show, Function<View, Boolean> clickCallback) { 194 if (isUseMaterial3FlagEnabled() && mDoc.isDirectory()) { 195 mPreviewIcon.setVisibility(View.GONE); 196 return; 197 } 198 mPreviewIcon.setVisibility(show ? View.VISIBLE : View.GONE); 199 if (show) { 200 mPreviewIcon.setContentDescription( 201 getPreviewIconContentDescription( 202 mIconHelper.shouldShowBadge(mDoc.userId.getIdentifier()), 203 mDoc.displayName, mDoc.userId)); 204 mPreviewIcon.setAccessibilityDelegate( 205 new PreviewAccessibilityDelegate(clickCallback)); 206 } 207 } 208 209 @Override bindBriefcaseIcon(boolean show)210 public void bindBriefcaseIcon(boolean show) { 211 mIconBadge.setVisibility(show ? View.VISIBLE : View.GONE); 212 } 213 214 @Override 215 @RequiresApi(Build.VERSION_CODES.S) bindProfileIcon(boolean show, int userIdIdentifier)216 public void bindProfileIcon(boolean show, int userIdIdentifier) { 217 Map<UserId, Drawable> userIdToBadgeMap = DocumentsApplication.getUserManagerState( 218 mContext).getUserIdToBadgeMap(); 219 Drawable drawable = userIdToBadgeMap.get(UserId.of(userIdIdentifier)); 220 mIconBadge.setImageDrawable(drawable); 221 mIconBadge.setVisibility(show ? View.VISIBLE : View.GONE); 222 mIconBadge.setContentDescription(mIconHelper.getProfileLabel(userIdIdentifier)); 223 } 224 225 @Override inDragRegion(MotionEvent event)226 public boolean inDragRegion(MotionEvent event) { 227 // Entire grid box should be draggable 228 return true; 229 } 230 231 @Override inSelectRegion(MotionEvent event)232 public boolean inSelectRegion(MotionEvent event) { 233 if (isUseMaterial3FlagEnabled()) { 234 return (mDoc.isDirectory() && !(mAction == State.ACTION_BROWSE)) ? false 235 : Views.isEventOver(event, itemView.getParent(), mIconWrapper); 236 } 237 return Views.isEventOver(event, itemView.getParent(), mIconLayout); 238 } 239 240 @Override inPreviewIconRegion(MotionEvent event)241 public boolean inPreviewIconRegion(MotionEvent event) { 242 return Views.isEventOver(event, itemView.getParent(), mPreviewIcon); 243 } 244 245 /** 246 * Bind this view to the given document for display. 247 * 248 * @param cursor Pointing to the item to be bound. 249 * @param modelId The model ID of the item. 250 */ 251 @Override bind(Cursor cursor, String modelId)252 public void bind(Cursor cursor, String modelId) { 253 assert (cursor != null); 254 255 mModelId = modelId; 256 257 mDoc.updateFromCursor(cursor, 258 UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID)), 259 getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY)); 260 261 mIconHelper.stopLoading(mIconThumb); 262 263 mIconMimeLg.animate().cancel(); 264 mIconMimeLg.setAlpha(1f); 265 mIconThumb.animate().cancel(); 266 mIconThumb.setAlpha(0f); 267 268 if (isUseMaterial3FlagEnabled()) { 269 mIconHelper.load( 270 mDoc, mIconThumb, mIconMimeLg, /* subIconMime= */ null, 271 thumbnailLoaded -> { 272 // Show stroke when thumbnail is loaded. 273 if (mIconWrapper != null) { 274 mIconWrapper.setStrokeWidth( 275 thumbnailLoaded ? mThumbnailStrokeWidth : 0); 276 } 277 }); 278 } else { 279 mIconHelper.load( 280 mDoc, mIconThumb, mIconMimeLg, mIconMimeSm, /* thumbnailLoadedCallback= */ 281 null); 282 } 283 284 mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE); 285 mTitle.setVisibility(View.VISIBLE); 286 287 // If file is partial, we want to show summary field as that's more relevant than fileSize 288 // and date 289 if (mDoc.isPartial()) { 290 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); 291 mDetails.setVisibility(View.VISIBLE); 292 mDate.setText(null); 293 mDetails.setText(docSummary); 294 } else { 295 if (mDoc.lastModified == -1) { 296 mDate.setText(null); 297 } else { 298 mDate.setText(Shared.formatTime(mContext, mDoc.lastModified)); 299 } 300 301 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); 302 if (mDoc.isDirectory() || docSize == -1) { 303 mDetails.setVisibility(View.GONE); 304 } else { 305 mDetails.setVisibility(View.VISIBLE); 306 mDetails.setText(Formatter.formatFileSize(mContext, docSize)); 307 } 308 } 309 310 if (mBullet != null && (mDetails.getText() == null || mDetails.getText().length() == 0 311 || mDate.getText() == null || mDate.getText().length() == 0)) { 312 // There is no need for the bullet separating the details and date. 313 mBullet.setVisibility(View.GONE); 314 } 315 } 316 } 317