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 
21 import static com.android.launcher3.Utilities.prefixTextWithIcon;
22 import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
23 
24 import android.annotation.TargetApi;
25 import android.app.ActivityOptions;
26 import android.content.ActivityNotFoundException;
27 import android.content.Intent;
28 import android.content.pm.LauncherApps;
29 import android.content.pm.LauncherApps.AppUsageLimit;
30 import android.icu.text.MeasureFormat;
31 import android.icu.text.MeasureFormat.FormatWidth;
32 import android.icu.util.Measure;
33 import android.icu.util.MeasureUnit;
34 import android.os.Build;
35 import android.os.UserHandle;
36 import android.util.Log;
37 import android.view.View;
38 import android.widget.TextView;
39 
40 import androidx.annotation.StringRes;
41 
42 import com.android.launcher3.BaseActivity;
43 import com.android.launcher3.BaseDraggingActivity;
44 import com.android.launcher3.R;
45 import com.android.launcher3.userevent.nano.LauncherLogProto;
46 import com.android.systemui.shared.recents.model.Task;
47 
48 import java.time.Duration;
49 import java.util.Locale;
50 
51 @TargetApi(Build.VERSION_CODES.Q)
52 public final class DigitalWellBeingToast {
53     static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
54     static final int MINUTE_MS = 60000;
55 
56     private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
57 
58     private final BaseDraggingActivity mActivity;
59     private final TaskView mTaskView;
60     private final LauncherApps mLauncherApps;
61 
62     private Task mTask;
63     private boolean mHasLimit;
64     private long mAppRemainingTimeMs;
65 
DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView)66     public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
67         mActivity = activity;
68         mTaskView = taskView;
69         mLauncherApps = activity.getSystemService(LauncherApps.class);
70     }
71 
setTaskFooter(View view)72     private void setTaskFooter(View view) {
73         View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view);
74         if (oldFooter != null) {
75             oldFooter.setOnClickListener(null);
76             mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter);
77         }
78     }
79 
setNoLimit()80     private void setNoLimit() {
81         mHasLimit = false;
82         mTaskView.setContentDescription(mTask.titleDescription);
83         setTaskFooter(null);
84         mAppRemainingTimeMs = 0;
85     }
86 
setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs)87     private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
88         mAppRemainingTimeMs = appRemainingTimeMs;
89         mHasLimit = true;
90         TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
91                 mActivity, mTaskView);
92         toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
93         toast.setOnClickListener(this::openAppUsageSettings);
94         setTaskFooter(toast);
95 
96         mTaskView.setContentDescription(
97                 getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
98         RecentsView rv = mTaskView.getRecentsView();
99         if (rv != null) {
100             rv.onDigitalWellbeingToastShown();
101         }
102     }
103 
getText()104     public String getText() {
105         return getText(mAppRemainingTimeMs);
106     }
107 
hasLimit()108     public boolean hasLimit() {
109         return mHasLimit;
110     }
111 
initialize(Task task)112     public void initialize(Task task) {
113         mTask = task;
114 
115         if (task.key.userId != UserHandle.myUserId()) {
116             setNoLimit();
117             return;
118         }
119 
120         THREAD_POOL_EXECUTOR.execute(() -> {
121             final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
122                     task.getTopComponent().getPackageName(),
123                     UserHandle.of(task.key.userId));
124 
125             final long appUsageLimitTimeMs =
126                     usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
127             final long appRemainingTimeMs =
128                     usageLimit != null ? usageLimit.getUsageRemaining() : -1;
129 
130             mTaskView.post(() -> {
131                 if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
132                     setNoLimit();
133                 } else {
134                     setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
135                 }
136             });
137         });
138     }
139 
getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId, boolean forceFormatWidth)140     private String getReadableDuration(
141             Duration duration,
142             FormatWidth formatWidthHourAndMinute,
143             @StringRes int durationLessThanOneMinuteStringId,
144             boolean forceFormatWidth) {
145         int hours = Math.toIntExact(duration.toHours());
146         int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
147 
148         // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
149         if (hours > 0 && minutes > 0) {
150             return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
151                     .formatMeasures(
152                             new Measure(hours, MeasureUnit.HOUR),
153                             new Measure(minutes, MeasureUnit.MINUTE));
154         }
155 
156         // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
157         if (hours > 0) {
158             return MeasureFormat.getInstance(
159                     Locale.getDefault(),
160                     forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
161                     .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
162         }
163 
164         // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
165         if (minutes > 0) {
166             return MeasureFormat.getInstance(
167                     Locale.getDefault()
168                     , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
169                     .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
170         }
171 
172         // Use a specific string for usage less than one minute but non-zero.
173         if (duration.compareTo(Duration.ZERO) > 0) {
174             return mActivity.getString(durationLessThanOneMinuteStringId);
175         }
176 
177         // Otherwise, return 0-minute string.
178         return MeasureFormat.getInstance(
179                 Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
180                 .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
181     }
182 
getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId)183     private String getReadableDuration(
184             Duration duration,
185             FormatWidth formatWidthHourAndMinute,
186             @StringRes int durationLessThanOneMinuteStringId) {
187         return getReadableDuration(
188                 duration,
189                 formatWidthHourAndMinute,
190                 durationLessThanOneMinuteStringId,
191                 /* forceFormatWidth= */ false);
192     }
193 
getRoundedUpToMinuteReadableDuration(long remainingTime)194     private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
195         final Duration duration = Duration.ofMillis(
196                 remainingTime > MINUTE_MS ?
197                         (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
198                         remainingTime);
199         return getReadableDuration(
200                 duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
201     }
202 
getText(long remainingTime)203     private String getText(long remainingTime) {
204         return mActivity.getString(
205                 R.string.time_left_for_app,
206                 getRoundedUpToMinuteReadableDuration(remainingTime));
207     }
208 
openAppUsageSettings(View view)209     public void openAppUsageSettings(View view) {
210         final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
211                 .putExtra(Intent.EXTRA_PACKAGE_NAME,
212                         mTask.getTopComponent().getPackageName()).addFlags(
213                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
214         try {
215             final BaseActivity activity = BaseActivity.fromContext(view.getContext());
216             final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
217                     view, 0, 0,
218                     view.getWidth(), view.getHeight());
219             activity.startActivity(intent, options.toBundle());
220             activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
221                     LauncherLogProto.ControlType.APP_USAGE_SETTINGS, view);
222         } catch (ActivityNotFoundException e) {
223             Log.e(TAG, "Failed to open app usage settings for task "
224                     + mTask.getTopComponent().getPackageName(), e);
225         }
226     }
227 
getContentDescriptionForTask( Task task, long appUsageLimitTimeMs, long appRemainingTimeMs)228     private String getContentDescriptionForTask(
229             Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
230         return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
231                 mActivity.getString(
232                         R.string.task_contents_description_with_remaining_time,
233                         task.titleDescription,
234                         getText(appRemainingTimeMs)) :
235                 task.titleDescription;
236     }
237 }
238