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