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