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