blob: 857654b54f7b3a988861eda6f0247a58afcf8bff [file] [log] [blame]
/*
* Copyright (C) 2015 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.car.systemupdater;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.storage.StorageEventListener;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.car.widget.ListItem;
import androidx.car.widget.ListItemAdapter;
import androidx.car.widget.ListItemProvider;
import androidx.car.widget.PagedListView;
import androidx.car.widget.TextListItem;
import androidx.fragment.app.Fragment;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
/**
* Display a list of files and directories.
*/
public class DeviceListFragment extends Fragment implements UpFragment {
private static final String TAG = "DeviceListFragment";
private static final String UPDATE_FILE_SUFFIX = ".zip";
private static final FileFilter UPDATE_FILE_FILTER =
file -> !file.isHidden() && (file.isDirectory()
|| file.getName().toLowerCase().endsWith(UPDATE_FILE_SUFFIX));
private final Stack<File> mFileStack = new Stack<>();
private StorageManager mStorageManager;
private SystemUpdater mSystemUpdater;
private List<File> mListItems;
private ListItemAdapter mAdapter;
private FileItemProvider mItemProvider;
private TextView mCurrentPathView;
private final StorageEventListener mListener = new StorageEventListener() {
@Override
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format(
"onVolumeMetadataChanged %d %d %s", oldState, newState, vol.toString()));
}
mFileStack.clear();
showMountedVolumes();
}
};
@Override
public void onAttach(Context context) {
super.onAttach(context);
mSystemUpdater = (SystemUpdater) context;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
mItemProvider = new FileItemProvider(context);
mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (mStorageManager == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to get StorageManager");
}
Toast.makeText(context, R.string.cannot_access_storage, Toast.LENGTH_LONG).show();
return;
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mAdapter = new ListItemAdapter(getContext(), mItemProvider);
return inflater.inflate(R.layout.folder_list, container, false);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
PagedListView folderListView = (PagedListView) view.findViewById(R.id.folder_list);
folderListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
folderListView.setAdapter(mAdapter);
mCurrentPathView = (TextView) view.findViewById(R.id.current_path);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
AppCompatActivity activity = (AppCompatActivity) getActivity();
ActionBar actionBar = activity.getSupportActionBar();
actionBar.setCustomView(R.layout.action_bar_with_button);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
showMountedVolumes();
}
@Override
public void onResume() {
super.onResume();
if (mStorageManager != null) {
mStorageManager.registerListener(mListener);
}
}
@Override
public void onPause() {
super.onPause();
if (mStorageManager != null) {
mStorageManager.unregisterListener(mListener);
}
}
/** Display the mounted volumes on this device. */
private void showMountedVolumes() {
if (mStorageManager == null) {
return;
}
final List<VolumeInfo> vols = mStorageManager.getVolumes();
ArrayList<File> volumes = new ArrayList<>(vols.size());
for (VolumeInfo vol : vols) {
File path = vol.getPathForUser(getActivity().getUserId());
if (vol.getState() == VolumeInfo.STATE_MOUNTED
&& vol.getType() == VolumeInfo.TYPE_PUBLIC
&& path != null) {
volumes.add(path);
}
}
// Otherwise show all of the available volumes.
mCurrentPathView.setText(getString(R.string.volumes, volumes.size()));
setFileList(volumes);
}
/** Set the list of files shown on the screen. */
private void setFileList(List<File> files) {
mListItems = files;
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
/** Handle user selection of a file. */
private void onFileSelected(File file) {
if (isUpdateFile(file)) {
mFileStack.clear();
mSystemUpdater.applyUpdate(file);
} else if (file.isDirectory()) {
showFolderContent(file);
mFileStack.push(file);
} else {
Toast.makeText(getContext(), R.string.invalid_file_type, Toast.LENGTH_LONG).show();
}
}
@Override
public boolean goUp() {
if (mFileStack.empty()) {
return false;
}
mFileStack.pop();
if (!mFileStack.empty()) {
// Show the list of files contained in the top of the stack.
showFolderContent(mFileStack.peek());
} else {
// When the stack is empty, display the volumes and reset the title.
showMountedVolumes();
}
return true;
}
/** Display the content at the provided {@code location}. */
private void showFolderContent(File folder) {
if (!folder.isDirectory()) {
// This should not happen.
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot show contents of a file.");
}
return;
}
mCurrentPathView.setText(getString(R.string.path, folder.getAbsolutePath()));
// Retrieve the list of files and update the displayed list.
new AsyncTask<File, Void, File[]>() {
@Override
protected File[] doInBackground(File... file) {
return file[0].listFiles(UPDATE_FILE_FILTER);
}
@Override
protected void onPostExecute(File[] results) {
super.onPostExecute(results);
if (results == null) {
results = new File[0];
Toast.makeText(getContext(), R.string.cannot_access_storage,
Toast.LENGTH_LONG).show();
}
setFileList(Arrays.asList(results));
}
}.execute(folder);
}
/** A list item provider to display the list of files on this fragment. */
private class FileItemProvider extends ListItemProvider {
private final Context mContext;
FileItemProvider(Context context) {
mContext = context;
}
@Override
public ListItem get(int position) {
if (position < 0 || position >= mListItems.size()) {
return null;
}
TextListItem item = new TextListItem(mContext);
File file = mListItems.get(position);
if (file != null) {
item.setTitle(file.getName());
item.setOnClickListener(v -> onFileSelected(file));
} else {
item.setTitle(getString(R.string.unknown_file));
}
return item;
}
@Override
public int size() {
return mListItems == null ? 0 : mListItems.size();
}
}
/** Returns true if a file is considered to contain a system update. */
private static boolean isUpdateFile(File file) {
return file.getName().endsWith(UPDATE_FILE_SUFFIX);
}
/** Used to request installation of an update. */
interface SystemUpdater {
/** Attempt to apply an update to the device contained in the {@code file}. */
void applyUpdate(File file);
}
}