• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.car.developeroptions.widget;
18 
19 import android.annotation.IdRes;
20 import android.annotation.UserIdInt;
21 import android.app.ActionBar;
22 import android.app.Activity;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageInfo;
27 import android.graphics.drawable.ColorDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.os.UserHandle;
31 import android.text.TextUtils;
32 import android.util.IconDrawableFactory;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.widget.ImageButton;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 
40 import androidx.annotation.IntDef;
41 import androidx.annotation.VisibleForTesting;
42 import androidx.fragment.app.Fragment;
43 import androidx.recyclerview.widget.RecyclerView;
44 
45 import com.android.car.developeroptions.R;
46 import com.android.car.developeroptions.Utils;
47 import com.android.car.developeroptions.applications.AppInfoBase;
48 import com.android.car.developeroptions.applications.appinfo.AppInfoDashboardFragment;
49 import com.android.car.developeroptions.overlay.FeatureFactory;
50 import com.android.settingslib.applications.ApplicationsState;
51 import com.android.settingslib.core.lifecycle.Lifecycle;
52 import com.android.settingslib.widget.ActionBarShadowController;
53 import com.android.settingslib.widget.LayoutPreference;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 
58 public class EntityHeaderController {
59 
60     @IntDef({ActionType.ACTION_NONE,
61             ActionType.ACTION_NOTIF_PREFERENCE,
62             ActionType.ACTION_EDIT_PREFERENCE,})
63     @Retention(RetentionPolicy.SOURCE)
64     public @interface ActionType {
65         int ACTION_NONE = 0;
66         int ACTION_NOTIF_PREFERENCE = 1;
67         int ACTION_EDIT_PREFERENCE = 2;
68     }
69 
70     public static final String PREF_KEY_APP_HEADER = "pref_app_header";
71 
72     private static final String TAG = "AppDetailFeature";
73 
74     private final Context mAppContext;
75     private final Activity mActivity;
76     private final Fragment mFragment;
77     private final int mMetricsCategory;
78     private final View mHeader;
79     private Lifecycle mLifecycle;
80     private RecyclerView mRecyclerView;
81     private Drawable mIcon;
82     private String mIconContentDescription;
83     private CharSequence mLabel;
84     private CharSequence mSummary;
85     // Required for hearing aid devices.
86     private CharSequence mSecondSummary;
87     private String mPackageName;
88     private Intent mAppNotifPrefIntent;
89     @UserIdInt
90     private int mUid = UserHandle.USER_NULL;
91     @ActionType
92     private int mAction1;
93     @ActionType
94     private int mAction2;
95 
96     private boolean mHasAppInfoLink;
97 
98     private boolean mIsInstantApp;
99 
100     private View.OnClickListener mEditOnClickListener;
101 
102     /**
103      * Creates a new instance of the controller.
104      *
105      * @param fragment The fragment that header will be placed in.
106      * @param header   Optional: header view if it's already created.
107      */
newInstance(Activity activity, Fragment fragment, View header)108     public static EntityHeaderController newInstance(Activity activity, Fragment fragment,
109             View header) {
110         return new EntityHeaderController(activity, fragment, header);
111     }
112 
EntityHeaderController(Activity activity, Fragment fragment, View header)113     private EntityHeaderController(Activity activity, Fragment fragment, View header) {
114         mActivity = activity;
115         mAppContext = activity.getApplicationContext();
116         mFragment = fragment;
117         mMetricsCategory = FeatureFactory.getFactory(mAppContext).getMetricsFeatureProvider()
118                 .getMetricsCategory(fragment);
119         if (header != null) {
120             mHeader = header;
121         } else {
122             mHeader = LayoutInflater.from(fragment.getContext())
123                     .inflate(R.layout.settings_entity_header, null /* root */);
124         }
125     }
126 
setRecyclerView(RecyclerView recyclerView, Lifecycle lifecycle)127     public EntityHeaderController setRecyclerView(RecyclerView recyclerView, Lifecycle lifecycle) {
128         mRecyclerView = recyclerView;
129         mLifecycle = lifecycle;
130         return this;
131     }
132 
133     /**
134      * Set the icon in the header. Callers should also consider calling setIconContentDescription
135      * to provide a description of this icon for accessibility purposes.
136      */
setIcon(Drawable icon)137     public EntityHeaderController setIcon(Drawable icon) {
138         if (icon != null) {
139             mIcon = icon.getConstantState().newDrawable(mAppContext.getResources());
140         }
141         return this;
142     }
143 
144     /**
145      * Convenience method to set the header icon from an ApplicationsState.AppEntry. Callers should
146      * also consider calling setIconContentDescription to provide a description of this icon for
147      * accessibility purposes.
148      */
setIcon(ApplicationsState.AppEntry appEntry)149     public EntityHeaderController setIcon(ApplicationsState.AppEntry appEntry) {
150         mIcon = IconDrawableFactory.newInstance(mAppContext).getBadgedIcon(appEntry.info);
151         return this;
152     }
153 
setIconContentDescription(String contentDescription)154     public EntityHeaderController setIconContentDescription(String contentDescription) {
155         mIconContentDescription = contentDescription;
156         return this;
157     }
158 
setLabel(CharSequence label)159     public EntityHeaderController setLabel(CharSequence label) {
160         mLabel = label;
161         return this;
162     }
163 
setLabel(ApplicationsState.AppEntry appEntry)164     public EntityHeaderController setLabel(ApplicationsState.AppEntry appEntry) {
165         mLabel = appEntry.label;
166         return this;
167     }
168 
setSummary(CharSequence summary)169     public EntityHeaderController setSummary(CharSequence summary) {
170         mSummary = summary;
171         return this;
172     }
173 
setSummary(PackageInfo packageInfo)174     public EntityHeaderController setSummary(PackageInfo packageInfo) {
175         if (packageInfo != null) {
176             mSummary = packageInfo.versionName;
177         }
178         return this;
179     }
180 
setSecondSummary(CharSequence summary)181     public EntityHeaderController setSecondSummary(CharSequence summary) {
182         mSecondSummary = summary;
183         return this;
184     }
185 
setSecondSummary(PackageInfo packageInfo)186     public EntityHeaderController setSecondSummary(PackageInfo packageInfo) {
187         if (packageInfo != null) {
188             mSummary = packageInfo.versionName;
189         }
190         return this;
191     }
192 
setHasAppInfoLink(boolean hasAppInfoLink)193     public EntityHeaderController setHasAppInfoLink(boolean hasAppInfoLink) {
194         mHasAppInfoLink = hasAppInfoLink;
195         return this;
196     }
197 
setButtonActions(@ctionType int action1, @ActionType int action2)198     public EntityHeaderController setButtonActions(@ActionType int action1,
199             @ActionType int action2) {
200         mAction1 = action1;
201         mAction2 = action2;
202         return this;
203     }
204 
setPackageName(String packageName)205     public EntityHeaderController setPackageName(String packageName) {
206         mPackageName = packageName;
207         return this;
208     }
209 
setUid(int uid)210     public EntityHeaderController setUid(int uid) {
211         mUid = uid;
212         return this;
213     }
214 
setAppNotifPrefIntent(Intent appNotifPrefIntent)215     public EntityHeaderController setAppNotifPrefIntent(Intent appNotifPrefIntent) {
216         mAppNotifPrefIntent = appNotifPrefIntent;
217         return this;
218     }
219 
setIsInstantApp(boolean isInstantApp)220     public EntityHeaderController setIsInstantApp(boolean isInstantApp) {
221         this.mIsInstantApp = isInstantApp;
222         return this;
223     }
224 
setEditListener(View.OnClickListener listener)225     public EntityHeaderController setEditListener(View.OnClickListener listener) {
226         this.mEditOnClickListener = listener;
227         return this;
228     }
229 
230     /**
231      * Done mutating entity header, rebinds everything and return a new {@link LayoutPreference}.
232      */
done(Activity activity, Context uiContext)233     public LayoutPreference done(Activity activity, Context uiContext) {
234         final LayoutPreference pref = new LayoutPreference(uiContext, done(activity));
235         // Makes sure it's the first preference onscreen.
236         pref.setOrder(-1000);
237         pref.setSelectable(false);
238         pref.setKey(PREF_KEY_APP_HEADER);
239         pref.setAllowDividerBelow(true);
240         return pref;
241     }
242 
243     /**
244      * Done mutating entity header, rebinds everything (optionally skip rebinding buttons).
245      */
done(Activity activity, boolean rebindActions)246     public View done(Activity activity, boolean rebindActions) {
247         styleActionBar(activity);
248         ImageView iconView = mHeader.findViewById(R.id.entity_header_icon);
249         if (iconView != null) {
250             iconView.setImageDrawable(mIcon);
251             iconView.setContentDescription(mIconContentDescription);
252         }
253         setText(R.id.entity_header_title, mLabel);
254         setText(R.id.entity_header_summary, mSummary);
255         setText(R.id.entity_header_second_summary, mSecondSummary);
256         if (mIsInstantApp) {
257             setText(R.id.install_type,
258                     mHeader.getResources().getString(R.string.install_type_instant));
259         }
260 
261         if (rebindActions) {
262             bindHeaderButtons();
263         }
264 
265         return mHeader;
266     }
267 
268     /**
269      * Only binds entity header with button actions.
270      */
bindHeaderButtons()271     public EntityHeaderController bindHeaderButtons() {
272         final View entityHeaderContent = mHeader.findViewById(R.id.entity_header_content);
273         final ImageButton button1 = mHeader.findViewById(android.R.id.button1);
274         final ImageButton button2 = mHeader.findViewById(android.R.id.button2);
275         bindAppInfoLink(entityHeaderContent);
276         bindButton(button1, mAction1);
277         bindButton(button2, mAction2);
278         return this;
279     }
280 
bindAppInfoLink(View entityHeaderContent)281     private void bindAppInfoLink(View entityHeaderContent) {
282         if (!mHasAppInfoLink) {
283             // Caller didn't ask for app link, skip.
284             return;
285         }
286         if (entityHeaderContent == null
287                 || mPackageName == null
288                 || mPackageName.equals(Utils.OS_PKG)
289                 || mUid == UserHandle.USER_NULL) {
290             Log.w(TAG, "Missing ingredients to build app info link, skip");
291             return;
292         }
293         entityHeaderContent.setOnClickListener(new View.OnClickListener() {
294             @Override
295             public void onClick(View v) {
296                 AppInfoBase.startAppInfoFragment(
297                         AppInfoDashboardFragment.class, R.string.application_info_label,
298                         mPackageName, mUid, mFragment, 0 /* request */,
299                         mMetricsCategory);
300             }
301         });
302         return;
303     }
304 
305     /**
306      * Styles the action bar (elevation, scrolling behaviors, color, etc).
307      * <p/>
308      * This method must be called after {@link Fragment#onCreate(Bundle)}.
309      */
styleActionBar(Activity activity)310     public EntityHeaderController styleActionBar(Activity activity) {
311         if (activity == null) {
312             Log.w(TAG, "No activity, cannot style actionbar.");
313             return this;
314         }
315         final ActionBar actionBar = activity.getActionBar();
316         if (actionBar == null) {
317             Log.w(TAG, "No actionbar, cannot style actionbar.");
318             return this;
319         }
320         actionBar.setBackgroundDrawable(
321                 new ColorDrawable(
322                         Utils.getColorAttrDefaultColor(activity, android.R.attr.colorPrimaryDark)));
323         actionBar.setElevation(0);
324         if (mRecyclerView != null && mLifecycle != null) {
325             ActionBarShadowController.attachToView(mActivity, mLifecycle, mRecyclerView);
326         }
327 
328         return this;
329     }
330 
331     /**
332      * Done mutating entity header, rebinds everything.
333      */
334     @VisibleForTesting
done(Activity activity)335     View done(Activity activity) {
336         return done(activity, true /* rebindActions */);
337     }
338 
bindButton(ImageButton button, @ActionType int action)339     private void bindButton(ImageButton button, @ActionType int action) {
340         if (button == null) {
341             return;
342         }
343         switch (action) {
344             case ActionType.ACTION_EDIT_PREFERENCE: {
345                 if (mEditOnClickListener == null) {
346                     button.setVisibility(View.GONE);
347                 } else {
348                     button.setImageResource(com.android.internal.R.drawable.ic_mode_edit);
349                     button.setVisibility(View.VISIBLE);
350                     button.setOnClickListener(mEditOnClickListener);
351                 }
352                 return;
353             }
354             case ActionType.ACTION_NOTIF_PREFERENCE: {
355                 if (mAppNotifPrefIntent == null) {
356                     button.setVisibility(View.GONE);
357                 } else {
358                     button.setOnClickListener(v -> {
359                         FeatureFactory.getFactory(mAppContext).getMetricsFeatureProvider()
360                                 .action(SettingsEnums.PAGE_UNKNOWN,
361                                         SettingsEnums.ACTION_OPEN_APP_NOTIFICATION_SETTING,
362                                         mMetricsCategory,
363                                         null, 0);
364                         mFragment.startActivity(mAppNotifPrefIntent);
365                     });
366                     button.setVisibility(View.VISIBLE);
367                 }
368                 return;
369             }
370             case ActionType.ACTION_NONE: {
371                 button.setVisibility(View.GONE);
372                 return;
373             }
374         }
375     }
376 
377 
setText(@dRes int id, CharSequence text)378     private void setText(@IdRes int id, CharSequence text) {
379         TextView textView = mHeader.findViewById(id);
380         if (textView != null) {
381             textView.setText(text);
382             textView.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
383         }
384     }
385 }
386