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 23 import android.annotation.TargetApi; 24 import android.app.ActivityOptions; 25 import android.content.ActivityNotFoundException; 26 import android.content.Intent; 27 import android.content.pm.LauncherApps; 28 import android.content.pm.LauncherApps.AppUsageLimit; 29 import android.icu.text.MeasureFormat; 30 import android.icu.text.MeasureFormat.FormatWidth; 31 import android.icu.util.Measure; 32 import android.icu.util.MeasureUnit; 33 import android.os.Build; 34 import android.os.UserHandle; 35 import android.util.Log; 36 import android.view.View; 37 import android.widget.TextView; 38 39 import androidx.annotation.StringRes; 40 41 import com.android.launcher3.BaseActivity; 42 import com.android.launcher3.BaseDraggingActivity; 43 import com.android.launcher3.R; 44 import com.android.launcher3.Utilities; 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 Utilities.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