1 // Copyright 2019 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.webapps.addtohomescreen; 6 7 import android.annotation.TargetApi; 8 import android.content.Context; 9 import android.content.res.Resources; 10 import android.graphics.Bitmap; 11 import android.graphics.drawable.Icon; 12 import android.os.Build; 13 import android.text.Editable; 14 import android.text.TextUtils; 15 import android.text.TextWatcher; 16 import android.view.LayoutInflater; 17 import android.view.View; 18 import android.widget.EditText; 19 import android.widget.ImageView; 20 import android.widget.LinearLayout; 21 import android.widget.RatingBar; 22 import android.widget.TextView; 23 24 import androidx.annotation.VisibleForTesting; 25 26 import org.chromium.base.ContextUtils; 27 import org.chromium.chrome.R; 28 import org.chromium.chrome.browser.banners.AppBannerManager; 29 import org.chromium.ui.modaldialog.DialogDismissalCause; 30 import org.chromium.ui.modaldialog.ModalDialogManager; 31 import org.chromium.ui.modaldialog.ModalDialogProperties; 32 import org.chromium.ui.modelutil.PropertyModel; 33 34 /** 35 * Displays the "Add to Homescreen" dialog, which contains a (possibly editable) title, icon, and 36 * possibly an origin. 37 * 38 * When the constructor is called, the dialog is shown immediately. A spinner is displayed if any 39 * data is not yet fetched, and accepting the dialog is disabled until all data is available and in 40 * its place on the screen. 41 */ 42 class AddToHomescreenDialogView implements View.OnClickListener, ModalDialogProperties.Controller { 43 private PropertyModel mDialogModel; 44 private ModalDialogManager mModalDialogManager; 45 @VisibleForTesting 46 protected AddToHomescreenViewDelegate mDelegate; 47 48 private View mParentView; 49 /** 50 * {@link #mShortcutTitleInput} and the {@link #mAppLayout} are mutually exclusive, depending on 51 * whether the home screen item is a bookmark shortcut or a web/native app. 52 */ 53 private EditText mShortcutTitleInput; 54 private LinearLayout mAppLayout; 55 private TextView mAppNameView; 56 private TextView mAppOriginView; 57 private RatingBar mAppRatingBar; 58 private ImageView mPlayLogoView; 59 60 private View mProgressBarView; 61 private ImageView mIconView; 62 63 private boolean mCanSubmit; 64 AddToHomescreenDialogView(Context context, ModalDialogManager modalDialogManager, AppBannerManager.InstallStringPair installStrings, AddToHomescreenViewDelegate delegate)65 AddToHomescreenDialogView(Context context, ModalDialogManager modalDialogManager, 66 AppBannerManager.InstallStringPair installStrings, 67 AddToHomescreenViewDelegate delegate) { 68 assert delegate != null; 69 70 mModalDialogManager = modalDialogManager; 71 mDelegate = delegate; 72 mParentView = LayoutInflater.from(context).inflate(R.layout.add_to_homescreen_dialog, null); 73 74 mProgressBarView = mParentView.findViewById(R.id.spinny); 75 mIconView = (ImageView) mParentView.findViewById(R.id.icon); 76 mShortcutTitleInput = mParentView.findViewById(R.id.text); 77 mAppLayout = (LinearLayout) mParentView.findViewById(R.id.app_info); 78 79 mAppNameView = (TextView) mAppLayout.findViewById(R.id.name); 80 mAppOriginView = (TextView) mAppLayout.findViewById(R.id.origin); 81 mAppRatingBar = (RatingBar) mAppLayout.findViewById(R.id.control_rating); 82 mPlayLogoView = (ImageView) mParentView.findViewById(R.id.play_logo); 83 84 mAppNameView.setOnClickListener(this); 85 mIconView.setOnClickListener(this); 86 87 mParentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 88 @Override 89 public void onLayoutChange(View v, int left, int top, int right, int bottom, 90 int oldLeft, int oldTop, int oldRight, int oldBottom) { 91 if (mProgressBarView.getMeasuredHeight() == mShortcutTitleInput.getMeasuredHeight() 92 && mShortcutTitleInput.getBackground() != null) { 93 // Force the text field to align better with the icon by accounting for the 94 // padding introduced by the background drawable. 95 mShortcutTitleInput.getLayoutParams().height = 96 mProgressBarView.getMeasuredHeight() 97 + mShortcutTitleInput.getPaddingBottom(); 98 v.requestLayout(); 99 v.removeOnLayoutChangeListener(this); 100 } 101 } 102 }); 103 104 // The "Add" button should be disabled if the dialog's text field is empty. 105 mShortcutTitleInput.addTextChangedListener(new TextWatcher() { 106 @Override 107 public void onTextChanged(CharSequence s, int start, int before, int count) {} 108 109 @Override 110 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 111 112 @Override 113 public void afterTextChanged(Editable editableText) { 114 updateInstallButton(); 115 } 116 }); 117 118 Resources resources = context.getResources(); 119 mDialogModel = 120 new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS) 121 .with(ModalDialogProperties.CONTROLLER, this) 122 .with(ModalDialogProperties.TITLE, resources, installStrings.titleTextId) 123 .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, resources, 124 installStrings.buttonTextId) 125 .with(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, true) 126 .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources, 127 R.string.cancel) 128 .with(ModalDialogProperties.CUSTOM_VIEW, mParentView) 129 .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true) 130 .build(); 131 mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.APP); 132 } 133 setTitle(String title)134 void setTitle(String title) { 135 mAppNameView.setText(title); 136 mShortcutTitleInput.setText(title); 137 } 138 setUrl(String url)139 void setUrl(String url) { 140 mAppOriginView.setText(url); 141 } 142 setIcon(Bitmap icon, boolean isAdaptive)143 void setIcon(Bitmap icon, boolean isAdaptive) { 144 if (isAdaptive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 145 setAdaptiveIcon(icon); 146 } else { 147 assert !isAdaptive : "Adaptive icons should not be provided pre-Android O."; 148 mIconView.setImageBitmap(icon); 149 } 150 mProgressBarView.setVisibility(View.GONE); 151 mIconView.setVisibility(View.VISIBLE); 152 } 153 154 @TargetApi(Build.VERSION_CODES.O) setAdaptiveIcon(Bitmap icon)155 private void setAdaptiveIcon(Bitmap icon) { 156 mIconView.setImageIcon(Icon.createWithAdaptiveBitmap(icon)); 157 } 158 setType(@ppType int type)159 void setType(@AppType int type) { 160 assert (type >= AppType.NATIVE && type <= AppType.SHORTCUT); 161 162 mShortcutTitleInput.setVisibility(type == AppType.SHORTCUT ? View.VISIBLE : View.GONE); 163 mAppLayout.setVisibility(type != AppType.SHORTCUT ? View.VISIBLE : View.GONE); 164 mAppOriginView.setVisibility(type == AppType.WEBAPK ? View.VISIBLE : View.GONE); 165 mAppRatingBar.setVisibility(type == AppType.NATIVE ? View.VISIBLE : View.GONE); 166 mPlayLogoView.setVisibility(type == AppType.NATIVE ? View.VISIBLE : View.GONE); 167 } 168 setNativeInstallButtonText(String installButtonText)169 void setNativeInstallButtonText(String installButtonText) { 170 mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT, installButtonText); 171 mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_CONTENT_DESCRIPTION, 172 ContextUtils.getApplicationContext().getString( 173 R.string.app_banner_view_native_app_install_accessibility, 174 installButtonText)); 175 } 176 setNativeAppRating(float rating)177 void setNativeAppRating(float rating) { 178 mAppRatingBar.setRating(rating); 179 mPlayLogoView.setImageResource(R.drawable.google_play); 180 } 181 setCanSubmit(boolean canSubmit)182 void setCanSubmit(boolean canSubmit) { 183 mCanSubmit = canSubmit; 184 updateInstallButton(); 185 } 186 updateInstallButton()187 private void updateInstallButton() { 188 boolean missingTitle = mShortcutTitleInput.getVisibility() == View.VISIBLE 189 && TextUtils.isEmpty(mShortcutTitleInput.getText()); 190 mDialogModel.set( 191 ModalDialogProperties.POSITIVE_BUTTON_DISABLED, !mCanSubmit || missingTitle); 192 } 193 194 /** 195 * From {@link View.OnClickListener}. Called when the views that have this class registered as 196 * their {@link View.OnClickListener} are clicked. 197 * 198 * @param v The view that was clicked. 199 */ 200 @Override onClick(View v)201 public void onClick(View v) { 202 if ((v == mAppNameView || v == mIconView)) { 203 if (mDelegate.onAppDetailsRequested()) { 204 mModalDialogManager.dismissDialog( 205 mDialogModel, DialogDismissalCause.ACTION_ON_CONTENT); 206 } 207 } 208 } 209 210 /** 211 * From {@link ModalDialogProperties.Controller}. Called when a dialog button is clicked. 212 * 213 * @param model The dialog model that is associated with this click event. 214 * @param buttonType The type of the button. 215 */ 216 @Override onClick(PropertyModel model, int buttonType)217 public void onClick(PropertyModel model, int buttonType) { 218 int dismissalCause = DialogDismissalCause.NEGATIVE_BUTTON_CLICKED; 219 if (buttonType == ModalDialogProperties.ButtonType.POSITIVE) { 220 mDelegate.onAddToHomescreen(mShortcutTitleInput.getText().toString()); 221 dismissalCause = DialogDismissalCause.POSITIVE_BUTTON_CLICKED; 222 } 223 mModalDialogManager.dismissDialog(mDialogModel, dismissalCause); 224 } 225 226 @Override onDismiss(PropertyModel model, int dismissalCause)227 public void onDismiss(PropertyModel model, int dismissalCause) { 228 if (dismissalCause == DialogDismissalCause.POSITIVE_BUTTON_CLICKED) return; 229 230 mDelegate.onViewDismissed(); 231 } 232 233 @VisibleForTesting getParentViewForTest()234 View getParentViewForTest() { 235 return mParentView; 236 } 237 } 238