1 /* 2 * Copyright (C) 2018 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.app.Notification; 19 import android.app.NotificationChannel; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.PowerManager; 29 import android.os.UpdateEngine; 30 import android.os.UpdateEngineCallback; 31 import android.text.format.Formatter; 32 import android.util.Log; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.TextView; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.StringRes; 40 import androidx.appcompat.app.AppCompatActivity; 41 import androidx.fragment.app.Fragment; 42 43 import com.android.car.ui.core.CarUi; 44 import com.android.car.ui.toolbar.MenuItem; 45 import com.android.car.ui.toolbar.ProgressBarController; 46 import com.android.car.ui.toolbar.ToolbarController; 47 import com.android.internal.util.Preconditions; 48 49 import java.io.File; 50 import java.io.IOException; 51 import java.util.Collections; 52 53 /** Display update state and progress. */ 54 public class UpdateLayoutFragment extends Fragment implements UpFragment { 55 public static final String EXTRA_RESUME_UPDATE = "resume_update"; 56 57 private static final String TAG = "UpdateLayoutFragment"; 58 private static final String EXTRA_UPDATE_FILE = "extra_update_file"; 59 private static final int PERCENT_MAX = 100; 60 private static final String REBOOT_REASON = "reboot-ab-update"; 61 private static final String NOTIFICATION_CHANNEL_ID = "update"; 62 private static final int NOTIFICATION_ID = 1; 63 64 private TextView mContentTitle; 65 private TextView mContentInfo; 66 private TextView mContentDetails; 67 private File mUpdateFile; 68 private ToolbarController mToolbar; 69 private ProgressBarController mProgressBar; 70 private PowerManager mPowerManager; 71 private NotificationManager mNotificationManager; 72 private final UpdateVerifier mPackageVerifier = new UpdateVerifier(); 73 private final UpdateEngine mUpdateEngine = new UpdateEngine(); 74 private boolean mInstallationInProgress = false; 75 76 private final CarUpdateEngineCallback mCarUpdateEngineCallback = new CarUpdateEngineCallback(); 77 78 /** Create a {@link UpdateLayoutFragment}. */ getInstance(File file)79 public static UpdateLayoutFragment getInstance(File file) { 80 UpdateLayoutFragment fragment = new UpdateLayoutFragment(); 81 Bundle bundle = new Bundle(); 82 bundle.putString(EXTRA_UPDATE_FILE, file.getAbsolutePath()); 83 fragment.setArguments(bundle); 84 return fragment; 85 } 86 87 /** Create a {@link UpdateLayoutFragment} showing an update in progress. */ newResumedInstance()88 public static UpdateLayoutFragment newResumedInstance() { 89 UpdateLayoutFragment fragment = new UpdateLayoutFragment(); 90 Bundle bundle = new Bundle(); 91 bundle.putBoolean(EXTRA_RESUME_UPDATE, true); 92 fragment.setArguments(bundle); 93 return fragment; 94 } 95 96 @Override onCreate(Bundle savedInstanceState)97 public void onCreate(Bundle savedInstanceState) { 98 super.onCreate(savedInstanceState); 99 100 if (!getArguments().getBoolean(EXTRA_RESUME_UPDATE)) { 101 mUpdateFile = new File(getArguments().getString(EXTRA_UPDATE_FILE)); 102 } 103 mPowerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); 104 mNotificationManager = 105 (NotificationManager) getContext().getSystemService(NotificationManager.class); 106 mNotificationManager.createNotificationChannel( 107 new NotificationChannel( 108 NOTIFICATION_CHANNEL_ID, 109 getContext().getString(R.id.system_update_auto_content_title), 110 NotificationManager.IMPORTANCE_DEFAULT)); 111 } 112 113 @Override onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)114 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 115 Bundle savedInstanceState) { 116 return inflater.inflate(R.layout.system_update_auto_content, container, false); 117 } 118 119 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)120 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 121 mContentTitle = view.findViewById(R.id.system_update_auto_content_title); 122 mContentInfo = view.findViewById(R.id.system_update_auto_content_info); 123 mContentDetails = view.findViewById(R.id.system_update_auto_content_details); 124 } 125 126 @Override onActivityCreated(Bundle savedInstanceState)127 public void onActivityCreated(Bundle savedInstanceState) { 128 super.onActivityCreated(savedInstanceState); 129 AppCompatActivity activity = (AppCompatActivity) getActivity(); 130 mToolbar = CarUi.requireToolbar(getActivity()); 131 mProgressBar = mToolbar.getProgressBar(); 132 mProgressBar.setIndeterminate(true); 133 mProgressBar.setVisible(true); 134 showStatus(R.string.verify_in_progress); 135 136 if (getArguments().getBoolean(EXTRA_RESUME_UPDATE)) { 137 // Rejoin the update already in progress. 138 showInstallationInProgress(); 139 } else { 140 // Extract the necessary information and begin the update. 141 mPackageVerifier.execute(mUpdateFile); 142 } 143 } 144 145 @Override onStop()146 public void onStop() { 147 super.onStop(); 148 if (mPackageVerifier != null) { 149 mPackageVerifier.cancel(true); 150 } 151 } 152 153 /** Update the status information. */ showStatus(@tringRes int status)154 private void showStatus(@StringRes int status) { 155 mContentTitle.setText(status); 156 if (mInstallationInProgress) { 157 mNotificationManager.notify(NOTIFICATION_ID, createNotification(getContext(), status)); 158 } else { 159 mNotificationManager.cancel(NOTIFICATION_ID); 160 } 161 } 162 163 /** Show the install now button. */ showInstallNow(UpdateParser.ParsedUpdate update)164 private void showInstallNow(UpdateParser.ParsedUpdate update) { 165 mContentTitle.setText(R.string.install_ready); 166 mContentInfo.append(getString(R.string.update_file_name, mUpdateFile.getName())); 167 mContentInfo.append(System.getProperty("line.separator")); 168 mContentInfo.append(getString(R.string.update_file_size)); 169 mContentInfo.append(Formatter.formatFileSize(getContext(), mUpdateFile.length())); 170 mContentDetails.setText(null); 171 MenuItem installButton = MenuItem.builder(getActivity()) 172 .setTitle(R.string.install_now) 173 .setOnClickListener(i -> installUpdate(update)) 174 .build(); 175 mToolbar.setMenuItems(Collections.singletonList(installButton)); 176 } 177 178 /** Reboot the system. */ rebootNow()179 private void rebootNow() { 180 if (Log.isLoggable(TAG, Log.INFO)) { 181 Log.i(TAG, "Rebooting Now."); 182 } 183 mPowerManager.reboot(REBOOT_REASON); 184 } 185 186 /** Attempt to install the update that is copied to the device. */ installUpdate(UpdateParser.ParsedUpdate parsedUpdate)187 private void installUpdate(UpdateParser.ParsedUpdate parsedUpdate) { 188 showInstallationInProgress(); 189 mUpdateEngine.applyPayload( 190 parsedUpdate.mUrl, parsedUpdate.mOffset, parsedUpdate.mSize, parsedUpdate.mProps); 191 } 192 193 /** Set the layout to show installation progress. */ showInstallationInProgress()194 private void showInstallationInProgress() { 195 mInstallationInProgress = true; 196 mProgressBar.setIndeterminate(false); 197 mProgressBar.setVisible(true); 198 mProgressBar.setMax(PERCENT_MAX); 199 mToolbar.setMenuItems(null); // Remove install button 200 showStatus(R.string.install_in_progress); 201 202 mUpdateEngine.bind(mCarUpdateEngineCallback, new Handler(getContext().getMainLooper())); 203 } 204 205 /** Attempt to verify the update and extract information needed for installation. */ 206 private class UpdateVerifier extends AsyncTask<File, Void, UpdateParser.ParsedUpdate> { 207 208 @Override doInBackground(File... files)209 protected UpdateParser.ParsedUpdate doInBackground(File... files) { 210 Preconditions.checkArgument(files.length > 0, "No file specified"); 211 File file = files[0]; 212 try { 213 return UpdateParser.parse(file); 214 } catch (IOException e) { 215 Log.e(TAG, String.format("For file %s", file), e); 216 return null; 217 } 218 } 219 220 @Override onPostExecute(UpdateParser.ParsedUpdate result)221 protected void onPostExecute(UpdateParser.ParsedUpdate result) { 222 mProgressBar.setVisible(false); 223 if (result == null) { 224 showStatus(R.string.verify_failure); 225 return; 226 } 227 if (!result.isValid()) { 228 showStatus(R.string.verify_failure); 229 Log.e(TAG, String.format("Failed verification %s", result)); 230 return; 231 } 232 if (Log.isLoggable(TAG, Log.INFO)) { 233 Log.i(TAG, result.toString()); 234 } 235 236 showInstallNow(result); 237 } 238 } 239 240 /** Handles events from the UpdateEngine. */ 241 public class CarUpdateEngineCallback extends UpdateEngineCallback { 242 243 @Override onStatusUpdate(int status, float percent)244 public void onStatusUpdate(int status, float percent) { 245 if (Log.isLoggable(TAG, Log.DEBUG)) { 246 Log.d(TAG, String.format("onStatusUpdate %d, Percent %.2f", status, percent)); 247 } 248 switch (status) { 249 case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT: 250 rebootNow(); 251 break; 252 case UpdateEngine.UpdateStatusConstants.DOWNLOADING: 253 mProgressBar.setProgress((int) (percent * 100)); 254 break; 255 default: 256 // noop 257 } 258 } 259 260 @Override onPayloadApplicationComplete(int errorCode)261 public void onPayloadApplicationComplete(int errorCode) { 262 Log.w(TAG, String.format("onPayloadApplicationComplete %d", errorCode)); 263 mInstallationInProgress = false; 264 showStatus(errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS 265 ? R.string.install_success 266 : R.string.install_failed); 267 mProgressBar.setVisible(false); 268 mToolbar.setMenuItems(null); // Remove install now button 269 } 270 } 271 272 /** Build a notification to show the installation status. */ createNotification(Context context, @StringRes int contents)273 private static Notification createNotification(Context context, @StringRes int contents) { 274 Intent intent = new Intent(); 275 intent.setComponent(new ComponentName(context, SystemUpdaterActivity.class)); 276 intent.putExtra(EXTRA_RESUME_UPDATE, true); 277 PendingIntent pendingIntent = 278 PendingIntent.getActivity( 279 context, 280 /* requestCode= */ 0, 281 intent, 282 PendingIntent.FLAG_UPDATE_CURRENT); 283 284 return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) 285 .setVisibility(Notification.VISIBILITY_PUBLIC) 286 .setContentTitle(context.getString(contents)) 287 .setSmallIcon(R.drawable.ic_system_update_alt_black_48dp) 288 .setContentIntent(pendingIntent) 289 .setShowWhen(false) 290 .setOngoing(true) 291 .setAutoCancel(false) 292 .build(); 293 } 294 } 295