1 /*
2  * Copyright (C) 2023 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 
17 package com.android.tv.feedbackconsent;
18 
19 import static com.android.tv.feedbackconsent.TvFeedbackConstants.BUGREPORT_CONSENT;
20 import static com.android.tv.feedbackconsent.TvFeedbackConstants.BUGREPORT_REQUESTED;
21 import static com.android.tv.feedbackconsent.TvFeedbackConstants.CANCEL_REQUEST;
22 import static com.android.tv.feedbackconsent.TvFeedbackConstants.CONSENT_RECEIVER;
23 import static com.android.tv.feedbackconsent.TvFeedbackConstants.RESULT_CODE_OK;
24 import static com.android.tv.feedbackconsent.TvFeedbackConstants.SYSTEM_LOGS_CONSENT;
25 import static com.android.tv.feedbackconsent.TvFeedbackConstants.SYSTEM_LOGS_KEY;
26 import static com.android.tv.feedbackconsent.TvFeedbackConstants.SYSTEM_LOGS_REQUESTED;
27 
28 import android.Manifest;
29 import android.app.Service;
30 import android.content.Intent;
31 import android.content.ActivityNotFoundException;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.ResultReceiver;
36 import android.os.IBinder;
37 import android.os.RemoteException;
38 import android.net.Uri;
39 import android.util.Log;
40 import android.annotation.RequiresPermission;
41 import android.annotation.NonNull;
42 import android.annotation.Nullable;
43 
44 import androidx.core.util.Preconditions;
45 
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.OutputStreamWriter;
50 import java.io.Writer;
51 import java.nio.charset.StandardCharsets;
52 import java.util.List;
53 
54 public final class TvFeedbackConsentService extends Service {
55 
56     private static final String TAG = TvFeedbackConsentService.class.getSimpleName();
57 
58     final TvDiagnosticInformationManagerBinder tvDiagnosticInformationBinder =
59         new TvDiagnosticInformationManagerBinder();
60 
61     @Override
onBind(Intent intent)62     public IBinder onBind(Intent intent) {
63         return tvDiagnosticInformationBinder;
64     }
65 
66     private final class TvDiagnosticInformationManagerBinder extends
67         ITvDiagnosticInformationManager.Stub {
68 
69         private boolean mSystemLogsRequested;
70         private boolean mBugreportRequested;
71         private boolean mSystemLogsConsented;
72         private ITvDiagnosticInformationManagerCallback mTvFeedbackConsentCallback;
73         private Uri mSystemLogsUri;
74         private Uri mBugreportUri;
75 
76         @RequiresPermission(allOf = {Manifest.permission.DUMP, Manifest.permission.READ_LOGS})
77         @Override
getDiagnosticInformation( @ullable Uri bugreportUri, @Nullable Uri systemLogsUri, @NonNull ITvDiagnosticInformationManagerCallback tvFeedbackConsentCallback)78         public void getDiagnosticInformation(
79             @Nullable Uri bugreportUri,
80             @Nullable Uri systemLogsUri,
81             @NonNull ITvDiagnosticInformationManagerCallback tvFeedbackConsentCallback) {
82             Preconditions.checkNotNull(tvFeedbackConsentCallback);
83             mTvFeedbackConsentCallback = tvFeedbackConsentCallback;
84             mBugreportRequested = false;
85             mSystemLogsRequested = false;
86             if (bugreportUri != null) {
87                 mBugreportRequested = true;
88                 mBugreportUri = bugreportUri;
89             }
90             if (systemLogsUri != null) {
91                 mSystemLogsRequested = true;
92                 mSystemLogsUri = systemLogsUri;
93             }
94             Preconditions.checkArgument(mBugreportRequested || mSystemLogsRequested,
95                 "No Diagnostic information requested: " +
96                     "Both bugreportUri and systemLogsUri cannot be null");
97 
98             ResultReceiver resultReceiver = createResultReceiver();
99             displayConsentScreen(resultReceiver);
100         }
101 
createResultReceiver()102         private ResultReceiver createResultReceiver() {
103             return new ResultReceiver(
104                 new Handler(Looper.getMainLooper())) {
105                 @Override
106                 protected void onReceiveResult(int resultCode, Bundle resultData) {
107                     if (mTvFeedbackConsentCallback == null) {
108                         Log.w(TAG, "Diagnostic information requested without a callback");
109                         return;
110                     }
111 
112                     if (resultData.getBoolean(CANCEL_REQUEST, false)) {
113                         try {
114                             Log.d(TAG, "User cancelled the request.");
115                             mTvFeedbackConsentCallback.onCancelRequest();
116                         } catch (RemoteException e) {
117                             throw new RuntimeException(e);
118                         }
119                     }
120 
121                     if (mBugreportRequested) {
122                         TvFeedbackBugreportHelper bugreportHelper =
123                             new TvFeedbackBugreportHelper(
124                                 /* context= */ TvFeedbackConsentService.this);
125                         bugreportHelper.startBugreport(
126                             resultData.getBoolean(BUGREPORT_CONSENT, false),
127                             mTvFeedbackConsentCallback,
128                             mBugreportUri);
129 
130                     }
131                     if (mSystemLogsRequested) {
132                         mSystemLogsConsented = resultData.getBoolean(SYSTEM_LOGS_CONSENT,
133                             false);
134                         List<String> systemLogs = resultData.getStringArrayList(
135                             SYSTEM_LOGS_KEY);
136 
137                         processSystemLogs(systemLogs);
138                     }
139                 }
140             };
141         }
142 
143         private void processSystemLogs(List<String> systemLogs) {
144             int systemLogsResultCode;
145             if (!mSystemLogsConsented) {
146                 systemLogsResultCode =
147                     mTvFeedbackConsentCallback.SYSTEM_LOGS_ERROR_USER_CONSENT_DENIED;
148                 Log.d(TAG, "User denied consent to share system logs");
149             } else if (systemLogs == null || systemLogs.isEmpty()) {
150                 systemLogsResultCode = mTvFeedbackConsentCallback.SYSTEM_LOGS_ERROR_RUNTIME;
151                 Log.d(TAG, "Error generating system logs");
152             } else {
153                 systemLogsResultCode = copySystemLogsToUri(systemLogs);
154             }
155 
156             try {
157                 if (systemLogsResultCode == RESULT_CODE_OK) {
158                     mTvFeedbackConsentCallback.onSystemLogsFinished();
159                     Log.d(TAG, "System logs generated and returned successfully.");
160                 } else {
161                     mTvFeedbackConsentCallback.onSystemLogsError(systemLogsResultCode);
162                 }
163 
164             } catch (RemoteException e) {
165                 throw new RuntimeException(e);
166             }
167         }
168 
169         private int copySystemLogsToUri(List<String> systemLogs) {
170             try (
171                 FileOutputStream out = (FileOutputStream) TvFeedbackConsentService.this
172                     .getContentResolver().openOutputStream(mSystemLogsUri, "w");
173                 Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
174                 for (String str : systemLogs) {
175                     writer.write(str);
176                     writer.write(System.lineSeparator());
177                 }
178                 return RESULT_CODE_OK;
179             } catch (FileNotFoundException e) {
180                 Log.e(TAG, "Uri file not found", e);
181                 return mTvFeedbackConsentCallback.SYSTEM_LOGS_ERROR_INVALID_INPUT;
182             } catch (IOException e) {
183                 Log.e(TAG, "Error copying system logs", e);
184                 return mTvFeedbackConsentCallback.SYSTEM_LOGS_ERROR_WRITE_FAILED;
185             }
186         }
187 
188         private void displayConsentScreen(ResultReceiver resultReceiver) {
189             Intent consentIntent = new Intent(
190                 TvFeedbackConsentService.this, TvFeedbackConsentActivity.class);
191             consentIntent.putExtra(CONSENT_RECEIVER, resultReceiver);
192             consentIntent.putExtra(BUGREPORT_REQUESTED, mBugreportRequested);
193             consentIntent.putExtra(SYSTEM_LOGS_REQUESTED, mSystemLogsRequested);
194             consentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
195 
196             try {
197                 TvFeedbackConsentService.this.startActivity(consentIntent);
198             } catch (ActivityNotFoundException e) {
199                 Log.e(TAG, "Error starting activity", e);
200             }
201         }
202     }
203 }