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.appcompat.app.ActionBar; 33 import androidx.appcompat.app.AppCompatActivity; 34 import androidx.car.widget.ListItem; 35 import androidx.car.widget.ListItemAdapter; 36 import androidx.car.widget.ListItemProvider; 37 import androidx.car.widget.PagedListView; 38 import androidx.car.widget.TextListItem; 39 import androidx.fragment.app.Fragment; 40 41 import java.io.File; 42 import java.io.FileFilter; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.Stack; 47 48 /** 49 * Display a list of files and directories. 50 */ 51 public class DeviceListFragment extends Fragment implements UpFragment { 52 53 private static final String TAG = "DeviceListFragment"; 54 private static final String UPDATE_FILE_SUFFIX = ".zip"; 55 private static final FileFilter UPDATE_FILE_FILTER = 56 file -> !file.isHidden() && (file.isDirectory() 57 || file.getName().toLowerCase().endsWith(UPDATE_FILE_SUFFIX)); 58 59 60 private final Stack<File> mFileStack = new Stack<>(); 61 private StorageManager mStorageManager; 62 private SystemUpdater mSystemUpdater; 63 private List<File> mListItems; 64 private ListItemAdapter mAdapter; 65 private FileItemProvider mItemProvider; 66 private TextView mCurrentPathView; 67 68 private final StorageEventListener mListener = new StorageEventListener() { 69 @Override 70 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 71 if (Log.isLoggable(TAG, Log.DEBUG)) { 72 Log.d(TAG, String.format( 73 "onVolumeMetadataChanged %d %d %s", oldState, newState, vol.toString())); 74 } 75 mFileStack.clear(); 76 showMountedVolumes(); 77 } 78 }; 79 80 @Override 81 public void onAttach(Context context) { 82 super.onAttach(context); 83 84 mSystemUpdater = (SystemUpdater) context; 85 } 86 87 @Override 88 public void onCreate(Bundle savedInstanceState) { 89 super.onCreate(savedInstanceState); 90 91 Context context = getContext(); 92 mItemProvider = new FileItemProvider(context); 93 94 mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); 95 if (mStorageManager == null) { 96 if (Log.isLoggable(TAG, Log.WARN)) { 97 Log.w(TAG, "Failed to get StorageManager"); 98 } 99 Toast.makeText(context, R.string.cannot_access_storage, Toast.LENGTH_LONG).show(); 100 return; 101 } 102 } 103 104 @Override 105 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 106 Bundle savedInstanceState) { 107 mAdapter = new ListItemAdapter(getContext(), mItemProvider); 108 return inflater.inflate(R.layout.folder_list, container, false); 109 } 110 111 @Override 112 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 113 PagedListView folderListView = (PagedListView) view.findViewById(R.id.folder_list); 114 folderListView.setMaxPages(PagedListView.ItemCap.UNLIMITED); 115 folderListView.setAdapter(mAdapter); 116 117 mCurrentPathView = (TextView) view.findViewById(R.id.current_path); 118 } 119 120 @Override 121 public void onActivityCreated(Bundle savedInstanceState) { 122 super.onActivityCreated(savedInstanceState); 123 AppCompatActivity activity = (AppCompatActivity) getActivity(); 124 ActionBar actionBar = activity.getSupportActionBar(); 125 actionBar.setCustomView(R.layout.action_bar_with_button); 126 actionBar.setDisplayShowCustomEnabled(true); 127 actionBar.setDisplayShowTitleEnabled(false); 128 129 showMountedVolumes(); 130 } 131 132 @Override 133 public void onResume() { 134 super.onResume(); 135 if (mStorageManager != null) { 136 mStorageManager.registerListener(mListener); 137 } 138 } 139 140 @Override 141 public void onPause() { 142 super.onPause(); 143 if (mStorageManager != null) { 144 mStorageManager.unregisterListener(mListener); 145 } 146 } 147 148 /** Display the mounted volumes on this device. */ 149 private void showMountedVolumes() { 150 if (mStorageManager == null) { 151 return; 152 } 153 final List<VolumeInfo> vols = mStorageManager.getVolumes(); 154 ArrayList<File> volumes = new ArrayList<>(vols.size()); 155 for (VolumeInfo vol : vols) { 156 File path = vol.getPathForUser(getActivity().getUserId()); 157 if (vol.getState() == VolumeInfo.STATE_MOUNTED 158 && vol.getType() == VolumeInfo.TYPE_PUBLIC 159 && path != null) { 160 volumes.add(path); 161 } 162 } 163 164 // Otherwise show all of the available volumes. 165 mCurrentPathView.setText(getString(R.string.volumes, volumes.size())); 166 setFileList(volumes); 167 } 168 169 /** Set the list of files shown on the screen. */ 170 private void setFileList(List<File> files) { 171 mListItems = files; 172 if (mAdapter != null) { 173 mAdapter.notifyDataSetChanged(); 174 } 175 } 176 177 /** Handle user selection of a file. */ 178 private void onFileSelected(File file) { 179 if (isUpdateFile(file)) { 180 mFileStack.clear(); 181 mSystemUpdater.applyUpdate(file); 182 } else if (file.isDirectory()) { 183 showFolderContent(file); 184 mFileStack.push(file); 185 } else { 186 Toast.makeText(getContext(), R.string.invalid_file_type, Toast.LENGTH_LONG).show(); 187 } 188 } 189 190 @Override 191 public boolean goUp() { 192 if (mFileStack.empty()) { 193 return false; 194 } 195 mFileStack.pop(); 196 if (!mFileStack.empty()) { 197 // Show the list of files contained in the top of the stack. 198 showFolderContent(mFileStack.peek()); 199 } else { 200 // When the stack is empty, display the volumes and reset the title. 201 showMountedVolumes(); 202 } 203 return true; 204 } 205 206 /** Display the content at the provided {@code location}. */ 207 private void showFolderContent(File folder) { 208 if (!folder.isDirectory()) { 209 // This should not happen. 210 if (Log.isLoggable(TAG, Log.DEBUG)) { 211 Log.d(TAG, "Cannot show contents of a file."); 212 } 213 return; 214 } 215 216 mCurrentPathView.setText(getString(R.string.path, folder.getAbsolutePath())); 217 218 // Retrieve the list of files and update the displayed list. 219 new AsyncTask<File, Void, File[]>() { 220 @Override 221 protected File[] doInBackground(File... file) { 222 return file[0].listFiles(UPDATE_FILE_FILTER); 223 } 224 225 @Override 226 protected void onPostExecute(File[] results) { 227 super.onPostExecute(results); 228 if (results == null) { 229 results = new File[0]; 230 Toast.makeText(getContext(), R.string.cannot_access_storage, 231 Toast.LENGTH_LONG).show(); 232 } 233 setFileList(Arrays.asList(results)); 234 } 235 }.execute(folder); 236 } 237 238 /** A list item provider to display the list of files on this fragment. */ 239 private class FileItemProvider extends ListItemProvider { 240 private final Context mContext; 241 242 FileItemProvider(Context context) { 243 mContext = context; 244 } 245 246 @Override 247 public ListItem get(int position) { 248 if (position < 0 || position >= mListItems.size()) { 249 return null; 250 } 251 TextListItem item = new TextListItem(mContext); 252 File file = mListItems.get(position); 253 if (file != null) { 254 item.setTitle(file.getName()); 255 item.setOnClickListener(v -> onFileSelected(file)); 256 } else { 257 item.setTitle(getString(R.string.unknown_file)); 258 } 259 return item; 260 } 261 262 @Override 263 public int size() { 264 return mListItems == null ? 0 : mListItems.size(); 265 } 266 } 267 268 /** Returns true if a file is considered to contain a system update. */ 269 private static boolean isUpdateFile(File file) { 270 return file.getName().endsWith(UPDATE_FILE_SUFFIX); 271 } 272 273 /** Used to request installation of an update. */ 274 interface SystemUpdater { 275 /** Attempt to apply an update to the device contained in the {@code file}. */ 276 void applyUpdate(File file); 277 } 278 } 279