1 /*
2  * Copyright 2019 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.example.android.locationattribution;
18 
19 import android.Manifest;
20 import android.content.Intent;
21 import android.content.SharedPreferences;
22 import android.content.pm.PackageManager;
23 import android.net.Uri;
24 import androidx.core.app.ActivityCompat;
25 import androidx.appcompat.app.AppCompatActivity;
26 import android.os.Bundle;
27 import android.text.Spannable;
28 import android.text.SpannableStringBuilder;
29 import android.text.TextPaint;
30 import android.text.method.LinkMovementMethod;
31 import android.text.style.ClickableSpan;
32 import android.text.style.URLSpan;
33 import android.util.Log;
34 import android.view.View;
35 import android.widget.Button;
36 import android.widget.TextView;
37 
38 /**
39  * Non-framework Location Attribution sample application.
40  *
41  * <p>This location attribution sample application demonstrates how to give user visibility
42  * and control of non-user-emergency location access by non-framework entities accessing GNSS
43  * chipset API directly bypassing the standard Android framework location permission settings.
44  *
45  * <p>Displays text to the user about the benefits of giving location permission to this app so
46  * that the non-framework entity or entities this app represents can access device location from
47  * GNSS chipset directly.
48  *
49  * <p>Provides a button to allow the user to modify the location permission settings for this app.
50  */
51 public class MainActivity extends AppCompatActivity {
52     private static final String APPLICATION_ID = "com.example.android.locationattribution";
53     private static final String TAG = "LocationAttribution";
54     private static final String PREFS_FILE_NAME = "LocationAttributionPrefs";
55     private static final int NON_FRAMEWORK_LOCATION_PERMISSION = 100;
56 
57     private static final String URL_PREFIX = "location_attribution_app://";
58     private static final String LINK_LEARN_MORE = URL_PREFIX + "learn_more";
59 
60     @Override
onCreate(Bundle savedInstanceState)61     protected void onCreate(Bundle savedInstanceState) {
62         super.onCreate(savedInstanceState);
63         setContentView(R.layout.activity_main);
64 
65         // Set non-framework location access use case description to display.
66         setTextViewAppInfoContent();
67 
68         // Show button for the user to modify location settings for this app.
69         Button button = (Button)findViewById(R.id.buttonModifyLocationSettings);
70         button.setOnClickListener(createModifyLocationSettingsButtonClickListener());
71     }
72 
setTextViewAppInfoContent()73     private void setTextViewAppInfoContent() {
74         // This text is seen by the user when this app is opened through the App info screen in
75         // Android Settings or when an intent is sent by carrier's own app.
76         TextView textViewAppInfo = findViewById(R.id.textViewAppInfo);
77         SpannableStringBuilder textViewAppInfoText = new SpannableStringBuilder();
78         for (CharSequence paragraph : getResources().getTextArray(
79                 R.array.textViewAppInfo_Paragraphs)) {
80             textViewAppInfoText.append(paragraph);
81         }
82 
83         replaceUrlSpansWithClickableSpans(textViewAppInfoText);
84         textViewAppInfo.setText(textViewAppInfoText);
85         textViewAppInfo.setMovementMethod(LinkMovementMethod.getInstance());
86     }
87 
createModifyLocationSettingsButtonClickListener()88     private View.OnClickListener createModifyLocationSettingsButtonClickListener() {
89         return new View.OnClickListener() {
90             @Override
91             public void onClick(View v) {
92                 if (isLocationPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
93                     if (!isLocationPermissionGranted(
94                             Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
95                         // Request 'Allow all the time' permission if the user didn't select
96                         // 'Don't ask again' option earlier.
97                         if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
98                                 Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
99                             showRequestBackgroundLocationPermissionDialog();
100                             return;
101                         }
102                     }
103 
104                     // We can't show tri-state dialog when permission is already granted.
105                     // So, go to the location permission settings screen directly.
106                     showLocationPermissionSettingsDashboard();
107                     return;
108                 }
109 
110                 if (isFirstTimeAskingLocationPermission()) {
111                     // Show tri-state dialog to change permission.
112                     setFirstTimeAskingLocationPermission(false);
113                     showRequestLocationPermissionDialog();
114                     return;
115                 }
116 
117                 if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
118                         Manifest.permission.ACCESS_FINE_LOCATION)) {
119                     // The user has previously denied the request. Show the tri-state dialog again.
120                     showRequestLocationPermissionDialog();
121                 } else {
122                     // User has denied permission and selected 'Don't ask again' option.
123                     showLocationPermissionSettingsDashboard();
124                 }
125             }
126         };
127     }
128 
129     private boolean isLocationPermissionGranted(String locationPermissionType) {
130         return ActivityCompat.checkSelfPermission(this, locationPermissionType)
131                 == PackageManager.PERMISSION_GRANTED;
132     }
133 
134     private void showRequestLocationPermissionDialog() {
135         ActivityCompat.requestPermissions(this,
136                 new String[]{Manifest.permission.ACCESS_FINE_LOCATION,
137                         Manifest.permission.ACCESS_BACKGROUND_LOCATION},
138                 NON_FRAMEWORK_LOCATION_PERMISSION);
139     }
140 
141     private void showRequestBackgroundLocationPermissionDialog() {
142         ActivityCompat.requestPermissions(this,
143                 new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
144                 NON_FRAMEWORK_LOCATION_PERMISSION);
145     }
146 
147     private void showLocationPermissionSettingsDashboard() {
148         startActivity(new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
149                 Uri.parse("package:" + APPLICATION_ID)));
150     }
151 
152     private void setFirstTimeAskingLocationPermission(boolean isFirstTime) {
153         SharedPreferences sharedPreference = getApplicationContext().getSharedPreferences(
154                 PREFS_FILE_NAME, MODE_PRIVATE);
155         SharedPreferences.Editor editor = sharedPreference.edit();
156         editor.putBoolean(Manifest.permission.ACCESS_FINE_LOCATION, isFirstTime).apply();
157         editor.commit();
158     }
159 
160     private boolean isFirstTimeAskingLocationPermission() {
161         return getApplicationContext().getSharedPreferences(PREFS_FILE_NAME,
162                 MODE_PRIVATE).getBoolean(Manifest.permission.ACCESS_FINE_LOCATION, true);
163     }
164 
165     /**
166      * A clickable text listener.
167      *
168      * <p>Used to listen to click events for clickable text in the description displayed by this
169      * activity's main screen and navigate to the appropriate screen based on the text link
170      * clicked.
171      */
172     private class AppInfoTextLinkClickableSpan extends ClickableSpan {
173         private final String mUrl;
174 
175         private AppInfoTextLinkClickableSpan(String url) {
176             mUrl = url;
177         }
178 
179         @Override
180         public void onClick(View textView) {
181             switch (mUrl) {
182                 case LINK_LEARN_MORE:
183                     startActivity(new Intent(Intent.ACTION_VIEW,
184                             Uri.parse(getString(R.string.urlLearnMore))));
185                     break;
186                 default:
187                     Log.e(TAG, "@string/textViewAppInfo contains invalid URL: " + mUrl);
188             }
189         }
190 
191         @Override
192         public void updateDrawState(TextPaint drawState) {
193             super.updateDrawState(drawState);
194             drawState.setUnderlineText(false);
195         }
196     }
197 
198     /*
199      * The description text in {@code textAppInfo} shown in the activity screen has URL links.
200      * Replace those links with clickable links so that we get notified when those links are
201      * clicked. We can then navigate to different screens based on the links clicked.
202      */
203     private void replaceUrlSpansWithClickableSpans(Spannable textAppInfo) {
204         for(URLSpan span: textAppInfo.getSpans(0, textAppInfo.length(), URLSpan.class)) {
205             int start = textAppInfo.getSpanStart(span);
206             int end = textAppInfo.getSpanEnd(span);
207             textAppInfo.removeSpan(span);
208             textAppInfo.setSpan(new AppInfoTextLinkClickableSpan(span.getURL()), start, end, 0);
209         }
210     }
211 }
212