/* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import android.view.LayoutInflater import android.view.MenuItem import android.view.ViewGroup import android.widget.PopupWindow import android.widget.ProgressBar import com.android.documentsui.base.Menus import com.android.documentsui.services.FileOperationService import com.android.documentsui.services.FileOperationService.EXTRA_PROGRESS import com.android.documentsui.services.Job import com.android.documentsui.services.JobProgress /** * JobPanelController is responsible for receiving broadcast updates from the [FileOperationService] * and updating a given menu item to reflect the current progress. */ class JobPanelController(private val mContext: Context) : BroadcastReceiver() { companion object { private const val TAG = "JobPanelController" private const val MAX_PROGRESS = 100 } private enum class State { INVISIBLE, INDETERMINATE, VISIBLE } /** The current state of the menu progress item. */ private var mState = State.INVISIBLE /** The total progress from 0 to MAX_PROGRESS. */ private var mTotalProgress = 0 /** List of jobs currently tracked by this class. */ private val mCurrentJobs = LinkedHashMap() /** Current menu item being controlled by this class. */ private var mMenuItem: MenuItem? = null init { val filter = IntentFilter(FileOperationService.ACTION_PROGRESS) mContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED) } private fun updateMenuItem(animate: Boolean) { mMenuItem?.let { Menus.setEnabledAndVisible(it, mState != State.INVISIBLE) val icon = it.actionView as ProgressBar when (mState) { State.INDETERMINATE -> icon.isIndeterminate = true State.VISIBLE -> icon.apply { isIndeterminate = false setProgress(mTotalProgress, animate) } State.INVISIBLE -> {} } } } /** * Sets the menu item controlled by this class. The item's actionView must be a [ProgressBar]. */ @Suppress("ktlint:standard:comment-wrapping") fun setMenuItem(menuItem: MenuItem) { val progressIcon = menuItem.actionView as ProgressBar progressIcon.max = MAX_PROGRESS progressIcon.setOnClickListener { view -> val panel = LayoutInflater.from(mContext).inflate( R.layout.job_progress_panel, /* root= */ null ) val popupWidth = mContext.resources.getDimension(R.dimen.job_progress_panel_width) + mContext.resources.getDimension(R.dimen.job_progress_panel_margin) val popup = PopupWindow( /* contentView= */ panel, /* width= */ popupWidth.toInt(), /* height= */ ViewGroup.LayoutParams.WRAP_CONTENT, /* focusable= */ true ) popup.showAsDropDown( /* anchor= */ view, /* xoff= */ view.width - popupWidth.toInt(), /* yoff= */ 0 ) } mMenuItem = menuItem updateMenuItem(animate = false) } override fun onReceive(context: Context?, intent: Intent) { val progresses = intent.getParcelableArrayListExtra( EXTRA_PROGRESS, JobProgress::class.java ) updateProgress(progresses!!) } private fun updateProgress(progresses: List) { var requiredBytes = 0L var currentBytes = 0L var allFinished = true for (jobProgress in progresses) { Log.d(TAG, "Received $jobProgress") mCurrentJobs.put(jobProgress.id, jobProgress) } for (jobProgress in mCurrentJobs.values) { if (jobProgress.state != Job.STATE_COMPLETED) { allFinished = false } if (jobProgress.requiredBytes != -1L && jobProgress.currentBytes != -1L) { requiredBytes += jobProgress.requiredBytes currentBytes += jobProgress.currentBytes } } if (mCurrentJobs.isEmpty()) { mState = State.INVISIBLE } else if (requiredBytes != 0L) { mState = State.VISIBLE mTotalProgress = (MAX_PROGRESS * currentBytes / requiredBytes).toInt() } else if (allFinished) { mState = State.VISIBLE mTotalProgress = MAX_PROGRESS } else { mState = State.INDETERMINATE } updateMenuItem(animate = true) } }