• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.quickstep.views;
18 
19 import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
20 import static android.view.Gravity.BOTTOM;
21 import static android.view.Gravity.CENTER_HORIZONTAL;
22 
23 import static com.android.launcher3.Utilities.prefixTextWithIcon;
24 import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
25 
26 import android.annotation.TargetApi;
27 import android.app.ActivityOptions;
28 import android.content.ActivityNotFoundException;
29 import android.content.Intent;
30 import android.content.pm.LauncherApps;
31 import android.content.pm.LauncherApps.AppUsageLimit;
32 import android.graphics.Outline;
33 import android.graphics.Paint;
34 import android.icu.text.MeasureFormat;
35 import android.icu.text.MeasureFormat.FormatWidth;
36 import android.icu.util.Measure;
37 import android.icu.util.MeasureUnit;
38 import android.os.Build;
39 import android.os.UserHandle;
40 import android.util.Log;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.ViewOutlineProvider;
44 import android.widget.FrameLayout;
45 import android.widget.TextView;
46 
47 import androidx.annotation.StringRes;
48 
49 import com.android.launcher3.BaseActivity;
50 import com.android.launcher3.BaseDraggingActivity;
51 import com.android.launcher3.R;
52 import com.android.launcher3.Utilities;
53 import com.android.systemui.shared.recents.model.Task;
54 
55 import java.time.Duration;
56 import java.util.Locale;
57 
58 @TargetApi(Build.VERSION_CODES.Q)
59 public final class DigitalWellBeingToast {
60     static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
61     static final int MINUTE_MS = 60000;
62 
63     private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
64 
65     private final BaseDraggingActivity mActivity;
66     private final TaskView mTaskView;
67     private final LauncherApps mLauncherApps;
68 
69     private Task mTask;
70     private boolean mHasLimit;
71     private long mAppRemainingTimeMs;
72     private View mBanner;
73     private ViewOutlineProvider mOldBannerOutlineProvider;
74     private float mBannerOffsetPercentage;
75     private float mVerticalOffset = 0f;
76 
DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView)77     public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
78         mActivity = activity;
79         mTaskView = taskView;
80         mLauncherApps = activity.getSystemService(LauncherApps.class);
81     }
82 
setNoLimit()83     private void setNoLimit() {
84         mHasLimit = false;
85         mTaskView.setContentDescription(mTask.titleDescription);
86         replaceBanner(null);
87         mAppRemainingTimeMs = 0;
88     }
89 
setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs)90     private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
91         mAppRemainingTimeMs = appRemainingTimeMs;
92         mHasLimit = true;
93         TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
94                 mActivity, mTaskView);
95         toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
96         toast.setOnClickListener(this::openAppUsageSettings);
97         replaceBanner(toast);
98 
99         mTaskView.setContentDescription(
100                 getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
101     }
102 
getText()103     public String getText() {
104         return getText(mAppRemainingTimeMs);
105     }
106 
hasLimit()107     public boolean hasLimit() {
108         return mHasLimit;
109     }
110 
initialize(Task task)111     public void initialize(Task task) {
112         mTask = task;
113 
114         if (task.key.userId != UserHandle.myUserId()) {
115             setNoLimit();
116             return;
117         }
118 
119         THREAD_POOL_EXECUTOR.execute(() -> {
120             final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
121                     task.getTopComponent().getPackageName(),
122                     UserHandle.of(task.key.userId));
123 
124             final long appUsageLimitTimeMs =
125                     usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
126             final long appRemainingTimeMs =
127                     usageLimit != null ? usageLimit.getUsageRemaining() : -1;
128 
129             mTaskView.post(() -> {
130                 if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
131                     setNoLimit();
132                 } else {
133                     setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
134                 }
135             });
136         });
137     }
138 
getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId, boolean forceFormatWidth)139     private String getReadableDuration(
140             Duration duration,
141             FormatWidth formatWidthHourAndMinute,
142             @StringRes int durationLessThanOneMinuteStringId,
143             boolean forceFormatWidth) {
144         int hours = Math.toIntExact(duration.toHours());
145         int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
146 
147         // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
148         if (hours > 0 && minutes > 0) {
149             return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
150                     .formatMeasures(
151                             new Measure(hours, MeasureUnit.HOUR),
152                             new Measure(minutes, MeasureUnit.MINUTE));
153         }
154 
155         // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
156         if (hours > 0) {
157             return MeasureFormat.getInstance(
158                     Locale.getDefault(),
159                     forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
160                     .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
161         }
162 
163         // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
164         if (minutes > 0) {
165             return MeasureFormat.getInstance(
166                     Locale.getDefault()
167                     , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
168                     .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
169         }
170 
171         // Use a specific string for usage less than one minute but non-zero.
172         if (duration.compareTo(Duration.ZERO) > 0) {
173             return mActivity.getString(durationLessThanOneMinuteStringId);
174         }
175 
176         // Otherwise, return 0-minute string.
177         return MeasureFormat.getInstance(
178                 Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
179                 .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
180     }
181 
getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId)182     private String getReadableDuration(
183             Duration duration,
184             FormatWidth formatWidthHourAndMinute,
185             @StringRes int durationLessThanOneMinuteStringId) {
186         return getReadableDuration(
187                 duration,
188                 formatWidthHourAndMinute,
189                 durationLessThanOneMinuteStringId,
190                 /* forceFormatWidth= */ false);
191     }
192 
getRoundedUpToMinuteReadableDuration(long remainingTime)193     private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
194         final Duration duration = Duration.ofMillis(
195                 remainingTime > MINUTE_MS ?
196                         (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
197                         remainingTime);
198         return getReadableDuration(
199                 duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
200     }
201 
getText(long remainingTime)202     private String getText(long remainingTime) {
203         return mActivity.getString(
204                 R.string.time_left_for_app,
205                 getRoundedUpToMinuteReadableDuration(remainingTime));
206     }
207 
openAppUsageSettings(View view)208     public void openAppUsageSettings(View view) {
209         final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
210                 .putExtra(Intent.EXTRA_PACKAGE_NAME,
211                         mTask.getTopComponent().getPackageName()).addFlags(
212                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
213         try {
214             final BaseActivity activity = BaseActivity.fromContext(view.getContext());
215             final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
216                     view, 0, 0,
217                     view.getWidth(), view.getHeight());
218             activity.startActivity(intent, options.toBundle());
219 
220             // TODO: add WW logging on the app usage settings click.
221         } catch (ActivityNotFoundException e) {
222             Log.e(TAG, "Failed to open app usage settings for task "
223                     + mTask.getTopComponent().getPackageName(), e);
224         }
225     }
226 
getContentDescriptionForTask( Task task, long appUsageLimitTimeMs, long appRemainingTimeMs)227     private String getContentDescriptionForTask(
228             Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
229         return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
230                 mActivity.getString(
231                         R.string.task_contents_description_with_remaining_time,
232                         task.titleDescription,
233                         getText(appRemainingTimeMs)) :
234                 task.titleDescription;
235     }
236 
replaceBanner(View view)237     private void replaceBanner(View view) {
238         resetOldBanner();
239         setBanner(view);
240     }
241 
resetOldBanner()242     private void resetOldBanner() {
243         if (mBanner != null) {
244             mBanner.setOutlineProvider(mOldBannerOutlineProvider);
245             mTaskView.removeView(mBanner);
246             mBanner.setOnClickListener(null);
247             mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
248         }
249     }
250 
setBanner(View view)251     private void setBanner(View view) {
252         mBanner = view;
253         if (view != null) {
254             setupAndAddBanner();
255             setBannerOutline();
256         }
257     }
258 
setupAndAddBanner()259     private void setupAndAddBanner() {
260         FrameLayout.LayoutParams layoutParams =
261                 (FrameLayout.LayoutParams) mBanner.getLayoutParams();
262         layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
263         layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
264                 mTaskView.getThumbnail().getLayoutParams()).bottomMargin;
265         mBanner.setTranslationY(mBannerOffsetPercentage * mBanner.getHeight());
266         mTaskView.addView(mBanner);
267     }
268 
setBannerOutline()269     private void setBannerOutline() {
270         mOldBannerOutlineProvider = mBanner.getOutlineProvider();
271         mBanner.setOutlineProvider(new ViewOutlineProvider() {
272             @Override
273             public void getOutline(View view, Outline outline) {
274                 mOldBannerOutlineProvider.getOutline(view, outline);
275                 outline.offset(0, Math.round(-view.getTranslationY() + mVerticalOffset));
276             }
277         });
278         mBanner.setClipToOutline(true);
279     }
280 
updateBannerOffset(float offsetPercentage, float verticalOffset)281     void updateBannerOffset(float offsetPercentage, float verticalOffset) {
282         if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) {
283             mVerticalOffset = verticalOffset;
284             mBannerOffsetPercentage = offsetPercentage;
285             mBanner.setTranslationY(offsetPercentage * mBanner.getHeight() + mVerticalOffset);
286             mBanner.invalidateOutline();
287         }
288     }
289 
setBannerColorTint(int color, float amount)290     void setBannerColorTint(int color, float amount) {
291         if (mBanner == null) {
292             return;
293         }
294         if (amount == 0) {
295             mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
296         }
297         Paint layerPaint = new Paint();
298         layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
299         mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
300         mBanner.setLayerPaint(layerPaint);
301     }
302 }
303