1 /* 2 * Copyright (C) 2015 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 package com.android.car.systemupdater; 17 18 import android.content.Context; 19 import android.os.AsyncTask; 20 import android.os.Bundle; 21 import android.os.storage.StorageEventListener; 22 import android.os.storage.StorageManager; 23 import android.os.storage.VolumeInfo; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.TextView; 29 import android.widget.Toast; 30 31 import androidx.annotation.NonNull; 32 import androidx.fragment.app.Fragment; 33 34 import com.android.car.ui.recyclerview.CarUiContentListItem; 35 import com.android.car.ui.recyclerview.CarUiListItem; 36 import com.android.car.ui.recyclerview.CarUiListItemAdapter; 37 import com.android.car.ui.recyclerview.CarUiRecyclerView; 38 39 import java.io.File; 40 import java.io.FileFilter; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.List; 44 import java.util.Stack; 45 46 /** 47 * Display a list of files and directories. 48 */ 49 public class DeviceListFragment extends Fragment implements UpFragment { 50 51 private static final String TAG = "DeviceListFragment"; 52 private static final String UPDATE_FILE_SUFFIX = ".zip"; 53 private static final FileFilter UPDATE_FILE_FILTER = 54 file -> !file.isHidden() && (file.isDirectory() 55 || file.getName().toLowerCase().endsWith(UPDATE_FILE_SUFFIX)); 56 57 58 private final Stack<File> mFileStack = new Stack<>(); 59 private StorageManager mStorageManager; 60 private SystemUpdater mSystemUpdater; 61 private CarUiRecyclerView mFolderListView; 62 private TextView mCurrentPathView; 63 64 private final StorageEventListener mListener = new StorageEventListener() { 65 @Override 66 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 67 if (Log.isLoggable(TAG, Log.DEBUG)) { 68 Log.d(TAG, String.format( 69 "onVolumeMetadataChanged %d %d %s", oldState, newState, vol.toString())); 70 } 71 mFileStack.clear(); 72 showMountedVolumes(); 73 } 74 }; 75 76 @Override onAttach(Context context)77 public void onAttach(Context context) { 78 super.onAttach(context); 79 80 mSystemUpdater = (SystemUpdater) context; 81 } 82 83 @Override onCreate(Bundle savedInstanceState)84 public void onCreate(Bundle savedInstanceState) { 85 super.onCreate(savedInstanceState); 86 Context context = getContext(); 87 88 mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); 89 if (mStorageManager == null) { 90 if (Log.isLoggable(TAG, Log.WARN)) { 91 Log.w(TAG, "Failed to get StorageManager"); 92 } 93 Toast.makeText(context, R.string.cannot_access_storage, Toast.LENGTH_LONG).show(); 94 return; 95 } 96 } 97 98 @Override onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)99 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 100 Bundle savedInstanceState) { 101 return inflater.inflate(R.layout.folder_list, container, false); 102 } 103 104 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)105 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 106 mFolderListView = view.findViewById(R.id.folder_list); 107 mCurrentPathView = view.findViewById(R.id.current_path); 108 } 109 110 @Override onActivityCreated(Bundle savedInstanceState)111 public void onActivityCreated(Bundle savedInstanceState) { 112 super.onActivityCreated(savedInstanceState); 113 showMountedVolumes(); 114 } 115 116 @Override onResume()117 public void onResume() { 118 super.onResume(); 119 if (mStorageManager != null) { 120 mStorageManager.registerListener(mListener); 121 } 122 } 123 124 @Override onPause()125 public void onPause() { 126 super.onPause(); 127 if (mStorageManager != null) { 128 mStorageManager.unregisterListener(mListener); 129 } 130 } 131 132 /** Display the mounted volumes on this device. */ showMountedVolumes()133 private void showMountedVolumes() { 134 if (mStorageManager == null) { 135 return; 136 } 137 final List<VolumeInfo> vols = mStorageManager.getVolumes(); 138 ArrayList<File> volumes = new ArrayList<>(vols.size()); 139 for (VolumeInfo vol : vols) { 140 File path = vol.getPathForUser(getActivity().getUserId()); 141 if (vol.getState() == VolumeInfo.STATE_MOUNTED 142 && vol.getType() == VolumeInfo.TYPE_PUBLIC 143 && path != null) { 144 volumes.add(path); 145 } 146 } 147 148 // Otherwise show all of the available volumes. 149 mCurrentPathView.setText(getString(R.string.volumes, volumes.size())); 150 setFileList(volumes); 151 } 152 153 /** Set the list of files shown on the screen. */ setFileList(List<File> files)154 private void setFileList(List<File> files) { 155 List<CarUiListItem> fileList = new ArrayList<>(); 156 for (File file : files) { 157 CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE); 158 item.setTitle(file.getName()); 159 item.setOnItemClickedListener(i -> onFileSelected(file)); 160 fileList.add(item); 161 } 162 163 CarUiListItemAdapter adapter = new CarUiListItemAdapter(fileList); 164 adapter.setMaxItems(CarUiRecyclerView.ItemCap.UNLIMITED); 165 mFolderListView.setAdapter(adapter); 166 } 167 168 /** Handle user selection of a file. */ onFileSelected(File file)169 private void onFileSelected(File file) { 170 if (isUpdateFile(file)) { 171 mFileStack.clear(); 172 mSystemUpdater.applyUpdate(file); 173 } else if (file.isDirectory()) { 174 showFolderContent(file); 175 mFileStack.push(file); 176 } else { 177 Toast.makeText(getContext(), R.string.invalid_file_type, Toast.LENGTH_LONG).show(); 178 } 179 } 180 181 @Override goUp()182 public boolean goUp() { 183 if (mFileStack.empty()) { 184 return false; 185 } 186 mFileStack.pop(); 187 if (!mFileStack.empty()) { 188 // Show the list of files contained in the top of the stack. 189 showFolderContent(mFileStack.peek()); 190 } else { 191 // When the stack is empty, display the volumes and reset the title. 192 showMountedVolumes(); 193 } 194 return true; 195 } 196 197 /** Display the content at the provided {@code location}. */ showFolderContent(File folder)198 private void showFolderContent(File folder) { 199 if (!folder.isDirectory()) { 200 // This should not happen. 201 if (Log.isLoggable(TAG, Log.DEBUG)) { 202 Log.d(TAG, "Cannot show contents of a file."); 203 } 204 return; 205 } 206 207 mCurrentPathView.setText(getString(R.string.path, folder.getAbsolutePath())); 208 209 // Retrieve the list of files and update the displayed list. 210 new AsyncTask<File, Void, File[]>() { 211 @Override 212 protected File[] doInBackground(File... file) { 213 return file[0].listFiles(UPDATE_FILE_FILTER); 214 } 215 216 @Override 217 protected void onPostExecute(File[] results) { 218 super.onPostExecute(results); 219 if (results == null) { 220 results = new File[0]; 221 Toast.makeText(getContext(), R.string.cannot_access_storage, 222 Toast.LENGTH_LONG).show(); 223 } 224 setFileList(Arrays.asList(results)); 225 } 226 }.execute(folder); 227 } 228 229 /** Returns true if a file is considered to contain a system update. */ isUpdateFile(File file)230 private static boolean isUpdateFile(File file) { 231 return file.getName().endsWith(UPDATE_FILE_SUFFIX); 232 } 233 234 /** Used to request installation of an update. */ 235 interface SystemUpdater { 236 /** Attempt to apply an update to the device contained in the {@code file}. */ applyUpdate(File file)237 void applyUpdate(File file); 238 } 239 } 240